/** * GoingsOn - Emails Compose * Compose/reply/forward flows, drafts, and send-with-delay (undo-send). * Extracted from emails.js. Reply/forward prefills are built in Rust * (build_reply_prefill / build_forward_prefill); this module owns the compose * window/modal chrome and the send lifecycle. Refreshing the list after a send * goes through GoingsOn.emails.load() (the controller keeps the list state). */ (function() { 'use strict'; const esc = GoingsOn.utils.escapeHtml; const escArg = GoingsOn.utils.escapeHandlerArg; const escAttr = GoingsOn.utils.escapeAttrValue; /** * Open the email compose interface (new window on desktop, modal on mobile). */ async function openCompose() { // On mobile, compose in-app since Tauri doesn't support multiple windows if (GoingsOn.touch?.isTouchDevice) { openComposeModal(); return; } // Open compose in a new window using Tauri invoke try { await GoingsOn.api.window.openCompose(); } catch (err) { console.error('Failed to open compose window:', err); // Fall back to in-app modal openComposeModal(); } } // Modal compose controller, populated after openComposeModal mounts and // composeForm.bindBehaviors wires up the shared behaviors. let modalComposeCtrl = null; function openComposeModal(prefill) { modalComposeCtrl = null; const accounts = GoingsOn.getEmailAccountsCache(); const pf = prefill || {}; // Stage 3/4 of compose unification, markup and behaviors come from // composeForm. The chrome (modal wrapper, footer action row, Attach // button) stays modal-specific. CC/BCC sit after To, matching compose.html. // Pre-append signature for the default/selected account; bindBehaviors // tracks it so the first From-change knows what trailing block to strip. const selectedAccount = accounts.find(a => a.id === pf.accountId) || accounts[0]; const selectedAccountId = selectedAccount?.id || null; const sig = selectedAccount?.emailSignature || ''; const bodyWithSig = (pf.body || '') + (sig ? '\n\n-- \n' + sig : ''); const ccPrefill = pf.cc || ''; const bccPrefill = pf.bcc || ''; const ccBccExpanded = !!(ccPrefill || bccPrefill); const fieldsHtml = GoingsOn.composeForm.buildFieldsHtml({ accounts, selectedAccountId, prefill: { to: pf.to || '', cc: ccPrefill, bcc: bccPrefill, subject: pf.subject || '', body: bodyWithSig, }, showCcBcc: ccBccExpanded, showAttachments: true, bodyRows: 8, }); const replyContext = { inReplyTo: pf.inReplyTo || null, references: pf.references || null, threadId: pf.threadId || null, }; const content = `
`; GoingsOn.modal.openModal('Compose Email', content); setTimeout(() => { const form = document.getElementById('compose-modal-form'); if (!form) return; // Stage 4/5, single bindBehaviors call owns autocomplete, // address highlight, CC/BCC toggle, signature swap, attachment // picker/render/remove, draft autosave, and reply indicator. modalComposeCtrl = GoingsOn.composeForm.bindBehaviors({ accounts, initialSignature: sig, getContacts: GoingsOn.autocomplete.getContacts, onError: (msg) => GoingsOn.ui.showToast(msg, 'error', { duration: 8000 }), enableAutosave: true, initialDraftId: pf.draftId || null, saveDraft: (input) => GoingsOn.api.emails.saveDraft(input), getReplyContext: () => replyContext, onDraftStatus: (kind, message) => { if (kind === 'error') { GoingsOn.ui.showToast(message, 'error', { duration: 6000 }); } else { GoingsOn.ui.showToast(message, 'success', { duration: 2500 }); } }, enableReplyIndicator: true, }); const attachBtn = document.getElementById('compose-modal-attach'); const cancelBtn = document.getElementById('compose-modal-cancel'); const sendBtn = document.getElementById('compose-modal-send'); const saveDraftBtn = document.getElementById('compose-modal-save-draft'); if (attachBtn) attachBtn.addEventListener('click', modalComposeCtrl.pickAttachment); if (saveDraftBtn) saveDraftBtn.addEventListener('click', () => modalComposeCtrl.saveDraftNow()); if (cancelBtn) cancelBtn.addEventListener('click', () => GoingsOn.ui.closeModal()); const submit = async () => { modalComposeCtrl.setSending(true); const accountSelect = document.getElementById('from-account'); const toEl = document.getElementById('to-address'); const ccEl = document.getElementById('cc-address'); const bccEl = document.getElementById('bcc-address'); const subjectEl = document.getElementById('subject'); const bodyEl = document.getElementById('body'); const attachedFiles = modalComposeCtrl.getAttachedFiles(); const input = GoingsOn.composeForm.collectInput({ accountId: accountSelect ? accountSelect.value : null, toAddress: toEl ? toEl.value : '', ccAddress: ccEl ? ccEl.value : '', bccAddress: bccEl ? bccEl.value : '', subject: subjectEl ? subjectEl.value : '', body: bodyEl ? bodyEl.value : '', attachedFiles, replyContext, }); const result = GoingsOn.composeForm.validateForSend(input, attachedFiles); if (!result.ok) { GoingsOn.ui.showToast(result.message, 'error', { duration: 8000 }); modalComposeCtrl.setSending(false); return; } GoingsOn.ui.closeModal(); queueSend({ input }); }; form.addEventListener('submit', (e) => { e.preventDefault(); submit(); }); if (sendBtn) sendBtn.addEventListener('click', (e) => { e.preventDefault(); submit(); }); }, 50); } async function openReply(emailId, replyAll) { try { // Rust builds the reply prefill: recipients (reply-all excludes our // own accounts + de-dupes), Re: subject, and quoted body. const pf = await GoingsOn.api.emails.buildReplyPrefill(emailId, !!replyAll); if (!pf) return; const prefill = { to: pf.to, subject: pf.subject, body: pf.body, inReplyTo: pf.inReplyTo || null, references: pf.references || null, threadId: pf.threadId || null, accountId: pf.accountId || '', }; if (GoingsOn.touch?.isTouchDevice) { openComposeModal(prefill); } else { await GoingsOn.api.window.openCompose(prefill); } } catch (err) { GoingsOn.ui.showToast(GoingsOn.utils.getErrorMessage(err, 'Failed to open reply'), 'error'); } } /** * Open compose window to forward an email. * @param {string} emailId - Email to forward */ async function openForward(emailId) { try { // Rust builds the Fwd: subject and forwarded-message body block. const pf = await GoingsOn.api.emails.buildForwardPrefill(emailId); if (!pf) return; const prefill = { to: '', subject: pf.subject, body: pf.body, accountId: pf.accountId || '' }; if (GoingsOn.touch?.isTouchDevice) { openComposeModal(prefill); } else { await GoingsOn.api.window.openCompose(prefill); } } catch (err) { GoingsOn.ui.showToast(GoingsOn.utils.getErrorMessage(err, 'Failed to forward email'), 'error'); } } // Drafts /** * Load and display drafts in a modal. */ async function openDraftsModal() { try { const drafts = await GoingsOn.api.emails.listDrafts(); if (drafts.length === 0) { GoingsOn.ui.openModal('Drafts', 'No drafts