{"record":{"id":"b5f6a5f914ed9ee8","repo":"odysseus-dev/odysseus","slug":"cleanup-failed","errorCode":null,"errorMessage":"Cleanup failed","messagePattern":"Cleanup failed","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"static/js/emailLibrary.js","lineNumber":1493,"sourceCode":"    return;\n  }\n  const restoreBusy = _setUnsubButtonBusy(btn, action === 'junk' ? 'Marking spam' : 'Deleting');\n  try {\n    const r = await fetch(emailApiUrl('/api/email/unsubscribe/cleanup', {\n      account_id: state._libAccountId || undefined,\n    }), {\n      method: 'POST',\n      credentials: 'same-origin',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        action,\n        uids,\n        folder: state._libFolder || 'INBOX',\n        account_id: state._libAccountId || '',\n      }),\n    });\n    const d = await r.json().catch(() => ({}));\n    if (!d.success) throw new Error(d.error || 'Cleanup failed');\n    const removed = new Set(uids.map(uid => String(uid)));\n    state._libEmails = state._libEmails.filter(e => !removed.has(String(e.uid)));\n    try { _libCacheWriteBack(); } catch (_) {}\n    try { _renderGrid(); } catch (_) {}\n    modal.querySelector('.email-unsub-followup')?.remove();\n    showToast?.(action === 'junk'\n      ? `Marked ${d.changed || 0} email${Number(d.changed || 0) === 1 ? '' : 's'} as spam`\n      : `Deleted ${d.changed || 0} email${Number(d.changed || 0) === 1 ? '' : 's'}`);\n  } catch (err) {\n    console.error(err);\n    showToast?.(err?.message || 'Cleanup failed');\n  } finally {\n    restoreBusy?.();\n  }\n}\n\nfunction _showUnsubscribeCleanupPrompt(modal, candidates) {\n  if (!modal || !Array.isArray(candidates) || !candidates.length) return;","sourceCodeStart":1475,"sourceCodeEnd":1511,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/static/js/emailLibrary.js#L1475-L1511","documentation":"Thrown when the bulk cleanup POST (delete/junk actions from the unsubscribe flow) answers with a body whose success flag is falsy. The fallback message 'Cleanup failed' appears only when the server sent no error field; the resulting toast shows err.message.","triggerScenarios":"Marking junk / deleting the batches of unsubscribe-sender emails: IMAP operation fails server-side (folder read-only, UIDSET invalid after a concurrent expunge, connection dropped), or the folder/account_id in the payload do not match the mailbox state.","commonSituations":"Another client expunged messages so the cached UIDs no longer resolve; server-side IMAP session invalidated mid-batch; account switched in the UI while the cleanup modal was open so account_id points at the wrong mailbox.","solutions":["Check server logs for the cleanup action's IMAP error — the JSON body usually carries it in d.error","Refresh the folder so the grid reflects current UIDs, then re-run cleanup","Ensure the account/folder were not changed while the modal was open","Retry after reconnecting the account if the IMAP session dropped"],"exampleFix":null,"handlingStrategy":"retry","validationCode":"const uids = selected.map(c => c.uid).filter(u => Number.isInteger(u));\nif (!uids.length) { showToast('Nothing selected'); return; }","typeGuard":null,"tryCatchPattern":"try { const d = await r.json().catch(() => ({})); if (!d.success) throw new Error(d.error || 'Cleanup failed'); ... } catch (err) { showToast(err?.message || 'Cleanup failed'); }","preventionTips":["Re-fetch UIDs immediately before bulk IMAP mutations — expunges invalidate cached ids","Pass folder/account_id captured at list-load time consistently (as done here)","Retry once on IMAP session drops before surfacing failure","Apply partial successes server-side and report d.changed so users know what succeeded"],"tags":["email","imap","bulk-action","uid-mismatch","unsubscribe"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}