fixed message rendering with html content ( white background)

This commit is contained in:
ghostersk
2026-03-07 17:09:41 +00:00
parent 6df2de5f22
commit 0bcd974b3d
6 changed files with 104 additions and 11 deletions

View File

@@ -134,6 +134,10 @@ func TestConnection(account *gomailModels.EmailAccount) error {
func (c *Client) Close() { c.imap.Logout() }
func (c *Client) DeleteMailbox(name string) error {
return c.imap.Delete(name)
}
func (c *Client) ListMailboxes() ([]*imap.MailboxInfo, error) {
ch := make(chan *imap.MailboxInfo, 64)
done := make(chan error, 1)

View File

@@ -404,6 +404,24 @@ func (h *APIHandler) CountFolderMessages(w http.ResponseWriter, r *http.Request)
func (h *APIHandler) DeleteFolder(w http.ResponseWriter, r *http.Request) {
userID := middleware.GetUserID(r)
folderID := pathInt64(r, "id")
// Look up folder before deleting so we have its path and account
folder, err := h.db.GetFolderByID(folderID)
if err != nil || folder == nil {
h.writeError(w, http.StatusNotFound, "folder not found")
return
}
// Delete on IMAP server first
account, err := h.db.GetAccount(folder.AccountID)
if err == nil && account != nil {
if imapClient, cerr := email.Connect(context.Background(), account); cerr == nil {
_ = imapClient.DeleteMailbox(folder.FullPath)
imapClient.Close()
}
}
// Delete from local DB
if err := h.db.DeleteFolder(folderID, userID); err != nil {
h.writeError(w, http.StatusInternalServerError, "delete failed")
return

View File

@@ -421,7 +421,17 @@ body.admin-page{overflow:auto;background:var(--bg)}
}
.inline-confirm.open{opacity:1;pointer-events:all;transform:translate(-50%,-50%)}
/* ── Folder no-sync indicator ────────────────────────────────── */
/* ── Context menu submenu ────────────────────────────────────── */
.ctx-has-sub{position:relative;justify-content:space-between}
.ctx-sub-arrow{margin-left:auto;font-size:12px;color:var(--muted);pointer-events:none}
.ctx-submenu{
display:none;position:absolute;left:100%;top:-4px;
background:var(--surface2);border:1px solid var(--border2);
border-radius:8px;padding:4px;min-width:160px;
box-shadow:0 8px 28px rgba(0,0,0,.55);z-index:210;
}
.ctx-has-sub:hover>.ctx-submenu{display:block}
.ctx-sub-item{white-space:nowrap}
.folder-nosync{opacity:.65}
/* ── Compose attach list ─────────────────────────────────────── */

View File

@@ -195,9 +195,45 @@ async function openEditAccount(id) {
connEl.style.display='none';
errEl.style.display=r.last_error?'block':'none';
if (r.last_error) errEl.textContent='Last sync error: '+r.last_error;
// Load hidden folders for this account
const hiddenEl = document.getElementById('edit-hidden-folders');
const hidden = S.folders.filter(f=>f.account_id===id && f.is_hidden);
if (!hidden.length) {
hiddenEl.innerHTML='<span style="color:var(--muted);font-size:12px">No hidden folders.</span>';
} else {
hiddenEl.innerHTML = hidden.map(f=>`
<div style="display:flex;align-items:center;justify-content:space-between;padding:5px 0;border-bottom:1px solid var(--border)">
<span style="font-size:13px">${esc(f.name)}</span>
<button class="btn-secondary" style="font-size:11px;padding:3px 10px" onclick="unhideFolder(${f.id})">Unhide</button>
</div>`).join('');
}
openModal('edit-account-modal');
}
async function unhideFolder(folderId) {
const f = S.folders.find(f=>f.id===folderId);
if (!f) return;
const r = await api('PUT','/folders/'+folderId+'/visibility',{is_hidden:false, sync_enabled:true});
if (r?.ok) {
toast('Folder restored to sidebar','success');
await loadFolders();
// Refresh hidden list in modal
const accId = parseInt(document.getElementById('edit-account-id').value);
if (accId) {
const hiddenEl = document.getElementById('edit-hidden-folders');
const hidden = S.folders.filter(f=>f.account_id===accId && f.is_hidden);
if (!hidden.length) hiddenEl.innerHTML='<span style="color:var(--muted);font-size:12px">No hidden folders.</span>';
else hiddenEl.innerHTML = hidden.map(f=>`
<div style="display:flex;align-items:center;justify-content:space-between;padding:5px 0;border-bottom:1px solid var(--border)">
<span style="font-size:13px">${esc(f.name)}</span>
<button class="btn-secondary" style="font-size:11px;padding:3px 10px" onclick="unhideFolder(${f.id})">Unhide</button>
</div>`).join('');
}
} else toast('Failed to unhide folder','error');
}
function toggleSyncDaysField() {
const mode=document.getElementById('edit-sync-mode')?.value;
const row=document.getElementById('edit-sync-days-row');
@@ -313,13 +349,20 @@ function showFolderMenu(e, folderId) {
const f = S.folders.find(f=>f.id===folderId);
if (!f) return;
const syncLabel = f.sync_enabled ? '⊘ Disable sync' : '↻ Enable sync';
const otherFolders = S.folders.filter(x=>x.id!==folderId&&x.account_id===f.account_id&&!x.is_hidden).slice(0,12);
const moveSub = otherFolders.map(x=>`<div class="ctx-item" style="padding-left:22px" onclick="moveFolderContents(${folderId},${x.id});closeMenu()">${esc(x.name)}</div>`).join('');
const otherFolders = S.folders.filter(x=>x.id!==folderId&&x.account_id===f.account_id&&!x.is_hidden).slice(0,16);
const moveItems = otherFolders.map(x=>
`<div class="ctx-item ctx-sub-item" onclick="moveFolderContents(${folderId},${x.id});closeMenu()">${esc(x.name)}</div>`
).join('');
const moveEntry = otherFolders.length ? `
<div class="ctx-item ctx-has-sub">📂 Move messages to
<span class="ctx-sub-arrow"></span>
<div class="ctx-submenu">${moveItems}</div>
</div>` : '';
showCtxMenu(e, `
<div class="ctx-item" onclick="syncFolderNow(${folderId});closeMenu()">↻ Sync this folder</div>
<div class="ctx-item" onclick="toggleFolderSync(${folderId});closeMenu()">${syncLabel}</div>
<div class="ctx-sep"></div>
${moveSub?`<div style="font-size:10px;color:var(--muted);padding:4px 12px;text-transform:uppercase;letter-spacing:.7px">Move messages to</div>${moveSub}<div class="ctx-sep"></div>`:''}
${moveEntry}
<div class="ctx-item" onclick="confirmHideFolder(${folderId});closeMenu()">👁 Hide from sidebar</div>
<div class="ctx-item danger" onclick="confirmDeleteFolder(${folderId});closeMenu()">🗑 Delete folder</div>`);
}
@@ -513,20 +556,36 @@ function renderMessageDetail(msg, showRemoteContent) {
const detail=document.getElementById('message-detail');
const allowed=showRemoteContent||S.remoteWhitelist.has(msg.from_email);
// CSS injected into every iframe — forces white background so dark-themed emails
// don't inherit our app's dark theme and become unreadable
const cssReset = `<style>html,body{background:#ffffff!important;color:#1a1a1a!important;` +
`font-family:Arial,sans-serif;font-size:14px;line-height:1.5;margin:8px}a{color:#1a5fb4}</style>`;
let bodyHtml='';
if (msg.body_html) {
if (allowed) {
const srcdoc = cssReset + msg.body_html;
bodyHtml=`<iframe id="msg-frame" sandbox="allow-same-origin allow-popups"
style="width:100%;border:none;min-height:300px;display:block"
srcdoc="${msg.body_html.replace(/"/g,'&quot;')}"></iframe>`;
srcdoc="${srcdoc.replace(/"/g,'&quot;')}"></iframe>`;
} else {
// Strip only remote resources (img src, background-image urls, external link/script)
// Keep full HTML structure so text remains readable
const stripped = msg.body_html
.replace(/<img(\s[^>]*?)src\s*=\s*(['"])[^'"]*\2/gi, '<img$1src=""data-blocked="1"')
.replace(/url\s*\(\s*(['"]?)https?:\/\/[^)'"]+\1\s*\)/gi, 'url()')
.replace(/<link[^>]*>/gi, '')
.replace(/<script[\s\S]*?<\/script>/gi, '');
const srcdoc = cssReset + stripped;
bodyHtml=`<div class="remote-content-banner">
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/></svg>
Remote images blocked.
<button class="rcb-btn" onclick="renderMessageDetail(S.currentMessage,true)">Load content</button>
<button class="rcb-btn" onclick="renderMessageDetail(S.currentMessage,true)">Load images</button>
<button class="rcb-btn" onclick="whitelistSender('${esc(msg.from_email)}')">Always allow from ${esc(msg.from_email)}</button>
</div>
<div class="detail-body-text">${esc(msg.body_text||'(empty)')}</div>`;
<iframe id="msg-frame" sandbox="allow-same-origin allow-popups"
style="width:100%;border:none;min-height:300px;display:block"
srcdoc="${srcdoc.replace(/"/g,'&quot;')}"></iframe>`;
}
} else {
bodyHtml=`<div class="detail-body-text">${esc(msg.body_text||'(empty)')}</div>`;

View File

@@ -245,6 +245,8 @@
</div>
<div id="edit-conn-result" class="test-result" style="display:none"></div>
<div id="edit-last-error" style="display:none" class="alert error"></div>
<div class="settings-group-title" style="margin:16px 0 8px">Hidden Folders</div>
<div id="edit-hidden-folders" style="font-size:12px;color:var(--muted)">Loading…</div>
<div class="modal-actions">
<button class="modal-cancel" onclick="closeModal('edit-account-modal')">Cancel</button>
<button class="btn-secondary" id="edit-test-btn" onclick="testEditConnection()">Test Connection</button>
@@ -300,5 +302,5 @@
{{end}}
{{define "scripts"}}
<script src="/static/js/app.js?v=7"></script>
<script src="/static/js/app.js?v=9"></script>
{{end}}

View File

@@ -5,12 +5,12 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{block "title" .}}GoMail{{end}}</title>
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display&family=DM+Sans:ital,wght@0,300;0,400;0,500;1,400&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/css/gomail.css?v=7">
<link rel="stylesheet" href="/static/css/gomail.css?v=9">
{{block "head_extra" .}}{{end}}
</head>
<body class="{{block "body_class" .}}{{end}}">
{{block "body" .}}{{end}}
<script src="/static/js/gomail.js?v=7"></script>
<script src="/static/js/gomail.js?v=9"></script>
{{block "scripts" .}}{{end}}
</body>
</html>