/** * GoingsOn - Email Accounts Module * Account CRUD, connection testing, sync, and OAuth flow. * Extends GoingsOn.emails with account management functions. */ (function() { 'use strict'; const esc = GoingsOn.utils.escapeHtml; const escAttr = GoingsOn.utils.escapeAttr; // ============ Account Form Builder ============ /** * Sync interval options used by both add and edit forms. */ const SYNC_INTERVAL_OPTIONS = [ { value: '', label: 'Disabled' }, { value: '5', label: 'Every 5 minutes' }, { value: '15', label: 'Every 15 minutes (default)' }, { value: '30', label: 'Every 30 minutes' }, { value: '60', label: 'Every hour' }, ]; /** * Build the shared IMAP/SMTP account form HTML. * @param {Object} opts * @param {string} opts.formId - Form element ID * @param {string} opts.idPrefix - ID prefix for inputs (e.g. 'acct' or 'edit-acct') * @param {string} opts.onSubmit - Form onsubmit attribute value * @param {Object} opts.values - Current field values (empty strings / defaults for add) * @param {boolean} opts.isEdit - Whether this is the edit form * @param {string} opts.submitLabel - Submit button label * @returns {string} HTML string for the form */ function buildAccountFormHtml(opts) { const { formId, idPrefix, onSubmit, values, isEdit, submitLabel } = opts; const passwordLabel = isEdit ? 'Password (leave empty to keep current)' : 'Password'; const passwordRequired = isEdit ? '' : 'required'; const passwordPlaceholder = isEdit ? 'Enter new password or leave empty' : 'your password'; const archiveHint = isEdit ? 'Use Test Connection to see available folders on this account.' : 'Gmail: [Gmail]/All Mail, Fastmail: Archive. Use Test Connection to see available folders.'; const syncOptionsHtml = SYNC_INTERVAL_OPTIONS.map(opt => { let selected = ''; if (isEdit) { // For edit: match against current value (null maps to '') const current = values.syncIntervalMinutes != null ? String(values.syncIntervalMinutes) : ''; selected = (opt.value === current) ? 'selected' : ''; } else { // For add: default to 15 minutes selected = (opt.value === '15') ? 'selected' : ''; } return ``; }).join(''); // For new accounts, check if server fields have been pre-filled (edit mode always shows them) const hasServerValues = isEdit || !!(values.imapServer || values.smtpServer); const advancedExpanded = hasServerValues ? 'expanded' : ''; const advancedHidden = hasServerValues ? '' : 'hidden'; const ff = GoingsOn.ui.renderFormField; const syncIntervalOpts = SYNC_INTERVAL_OPTIONS.map(opt => { let selected; if (isEdit) { const current = values.syncIntervalMinutes != null ? String(values.syncIntervalMinutes) : ''; selected = opt.value === current; } else { selected = opt.value === '15'; } return { value: opt.value, label: opt.label, selected }; }); return `
${ff({ kind: 'text', name: 'account_name', id: `${idPrefix}-name`, label: 'Account Name', value: values.accountName || '', placeholder: 'Personal, Work, etc.', required: true })}
${ff({ kind: 'text', name: 'username', id: `${idPrefix}-username`, label: 'Username', value: values.username || '', placeholder: 'Usually your email address', required: true })} ${ff({ kind: 'password', name: 'password', id: `${idPrefix}-password`, label: passwordLabel, placeholder: passwordPlaceholder, required: !isEdit })} ${ff({ kind: 'text', name: 'archive_folder_name', id: `${idPrefix}-archive-folder`, label: 'Archive Folder Name', value: values.archiveFolderName || 'Archive', placeholder: 'Archive', hint: archiveHint })} ${ff({ kind: 'textarea', name: 'email_signature', id: `${idPrefix}-signature`, label: 'Email Signature', value: values.emailSignature || '', placeholder: '-- \nYour Name', hint: 'Appended to outbound emails. Plain text only.', attrs: { rows: 3 } })}
${ff({ kind: 'text', name: 'imap_server', id: `${idPrefix}-imap-server`, label: 'IMAP Server', value: values.imapServer || '', placeholder: 'imap.example.com', required: true })} ${ff({ kind: 'number', name: 'imap_port', id: `${idPrefix}-imap-port`, label: 'IMAP Port', value: values.imapPort || 993, required: true })}
${ff({ kind: 'text', name: 'smtp_server', id: `${idPrefix}-smtp-server`, label: 'SMTP Server', value: values.smtpServer || '', placeholder: 'smtp.example.com', required: true })} ${ff({ kind: 'number', name: 'smtp_port', id: `${idPrefix}-smtp-port`, label: 'SMTP Port', value: values.smtpPort || 587, required: true })}
Show a system notification when new emails arrive during auto-sync. Off by default.
${ff({ kind: 'select', name: 'sync_interval_minutes', id: `${idPrefix}-sync-interval`, label: 'Auto-sync Interval', options: syncIntervalOpts, hint: 'Automatically check for new emails at this interval.' })}
`; } // ============ IMAP/SMTP Auto-Detect ============ /** * Well-known email provider server settings. * Key is the email domain; value has imap, smtp, archive defaults. */ // App-password notes shared across providers that require them. The note is shown // beneath the email field when the domain is detected; the link opens in the OS // browser via the regular anchor (Tauri allows external http(s) targets). const NOTE_APPLE = 'Apple requires an app-specific password for third-party mail apps — your normal iCloud password will not work. Generate one at appleid.apple.com → Sign-In and Security → App-Specific Passwords, then paste it in the Password field below.'; const NOTE_FASTMAIL = 'Fastmail requires an app password (not your normal password). Create one at fastmail.com → Settings → Privacy & Security → Integrations, then paste it in the Password field below.'; const NOTE_GOOGLE = 'Gmail requires an app password (your normal Google password will not work). With 2-Step Verification enabled, create one at myaccount.google.com/apppasswords and paste it below.'; const NOTE_YAHOO = 'Yahoo requires an app password. Create one at Yahoo Account Security → Generate app password and paste it below.'; const NOTE_AOL = 'AOL requires an app password. Create one at AOL Account Security → Generate app password and paste it below.'; const NOTE_OUTLOOK = 'If your Microsoft account has 2-Step Verification on, generate an app password at account.microsoft.com/security → Advanced security options. Otherwise your normal password works.'; const NOTE_PROTON = 'Proton Mail does not allow direct IMAP/SMTP — you must run Proton Bridge locally and use the bridge-generated credentials. Bridge is a paid plan feature.'; const PROVIDER_SETTINGS = { 'gmail.com': { imap: 'imap.gmail.com', imapPort: 993, smtp: 'smtp.gmail.com', smtpPort: 587, archive: '[Gmail]/All Mail', name: 'Gmail', note: NOTE_GOOGLE }, 'googlemail.com': { imap: 'imap.gmail.com', imapPort: 993, smtp: 'smtp.gmail.com', smtpPort: 587, archive: '[Gmail]/All Mail', name: 'Gmail', note: NOTE_GOOGLE }, 'fastmail.com': { imap: 'imap.fastmail.com', imapPort: 993, smtp: 'smtp.fastmail.com', smtpPort: 587, archive: 'Archive', name: 'Fastmail', note: NOTE_FASTMAIL }, 'outlook.com': { imap: 'outlook.office365.com', imapPort: 993, smtp: 'smtp.office365.com', smtpPort: 587, archive: 'Archive', name: 'Outlook', note: NOTE_OUTLOOK }, 'hotmail.com': { imap: 'outlook.office365.com', imapPort: 993, smtp: 'smtp.office365.com', smtpPort: 587, archive: 'Archive', name: 'Hotmail', note: NOTE_OUTLOOK }, 'live.com': { imap: 'outlook.office365.com', imapPort: 993, smtp: 'smtp.office365.com', smtpPort: 587, archive: 'Archive', name: 'Outlook', note: NOTE_OUTLOOK }, 'yahoo.com': { imap: 'imap.mail.yahoo.com', imapPort: 993, smtp: 'smtp.mail.yahoo.com', smtpPort: 587, archive: 'Archive', name: 'Yahoo', note: NOTE_YAHOO }, 'icloud.com': { imap: 'imap.mail.me.com', imapPort: 993, smtp: 'smtp.mail.me.com', smtpPort: 587, archive: 'Archive', name: 'iCloud', note: NOTE_APPLE }, 'me.com': { imap: 'imap.mail.me.com', imapPort: 993, smtp: 'smtp.mail.me.com', smtpPort: 587, archive: 'Archive', name: 'iCloud', note: NOTE_APPLE }, 'mac.com': { imap: 'imap.mail.me.com', imapPort: 993, smtp: 'smtp.mail.me.com', smtpPort: 587, archive: 'Archive', name: 'iCloud', note: NOTE_APPLE }, 'protonmail.com': { imap: 'imap.protonmail.ch', imapPort: 993, smtp: 'smtp.protonmail.ch', smtpPort: 587, archive: 'Archive', name: 'Proton Mail', note: NOTE_PROTON }, 'proton.me': { imap: 'imap.protonmail.ch', imapPort: 993, smtp: 'smtp.protonmail.ch', smtpPort: 587, archive: 'Archive', name: 'Proton Mail', note: NOTE_PROTON }, 'zoho.com': { imap: 'imap.zoho.com', imapPort: 993, smtp: 'smtp.zoho.com', smtpPort: 587, archive: 'Archive', name: 'Zoho' }, 'aol.com': { imap: 'imap.aol.com', imapPort: 993, smtp: 'smtp.aol.com', smtpPort: 587, archive: 'Archive', name: 'AOL', note: NOTE_AOL }, }; /** * Attach auto-detect behavior to an email input field. * When the user types a recognized domain, auto-fills server fields. * @param {string} idPrefix - The form field ID prefix (e.g. 'acct') */ function attachAutoDetect(idPrefix) { const emailEl = document.getElementById(`${idPrefix}-email`); if (!emailEl) return; let lastDetectedDomain = null; const detect = () => { const email = emailEl.value.trim(); const domain = email.split('@')[1]?.toLowerCase(); if (!domain || domain === lastDetectedDomain) return; const settings = PROVIDER_SETTINGS[domain]; const statusEl = document.getElementById(`${idPrefix}-detect-status`); const noteEl = document.getElementById(`${idPrefix}-detect-note`); if (!settings) { if (statusEl && domain.includes('.')) { statusEl.textContent = 'Unknown provider — fill in server details under Advanced settings'; statusEl.style.color = 'var(--text-secondary)'; } if (noteEl) { noteEl.classList.add('hidden'); noteEl.innerHTML = ''; } return; } lastDetectedDomain = domain; const imapEl = document.getElementById(`${idPrefix}-imap-server`); const imapPortEl = document.getElementById(`${idPrefix}-imap-port`); const smtpEl = document.getElementById(`${idPrefix}-smtp-server`); const smtpPortEl = document.getElementById(`${idPrefix}-smtp-port`); const usernameEl = document.getElementById(`${idPrefix}-username`); const archiveEl = document.getElementById(`${idPrefix}-archive-folder`); // Auto-fill server fields if (imapEl && (!imapEl.value || imapEl.value === 'imap.example.com')) imapEl.value = settings.imap; if (imapPortEl && (imapPortEl.value === '993' || !imapPortEl.value)) imapPortEl.value = settings.imapPort; if (smtpEl && (!smtpEl.value || smtpEl.value === 'smtp.example.com')) smtpEl.value = settings.smtp; if (smtpPortEl && (smtpPortEl.value === '587' || !smtpPortEl.value)) smtpPortEl.value = settings.smtpPort; if (usernameEl && !usernameEl.value) usernameEl.value = email; if (archiveEl && (archiveEl.value === 'Archive' || !archiveEl.value)) archiveEl.value = settings.archive; if (statusEl) { statusEl.textContent = `Detected ${settings.name || domain} — server settings auto-filled`; statusEl.style.color = 'var(--accent-green)'; } if (noteEl) { if (settings.note) { noteEl.innerHTML = settings.note; noteEl.classList.remove('hidden'); } else { noteEl.classList.add('hidden'); noteEl.innerHTML = ''; } } }; emailEl.addEventListener('input', detect); emailEl.addEventListener('change', detect); } // ============ Email Accounts ============ async function loadAccounts() { try { const accounts = await GoingsOn.api.emailAccounts.list(); GoingsOn.state.set('emailAccounts', accounts); } catch (err) { console.error('Failed to load email accounts:', err); GoingsOn.state.set('emailAccounts', []); } } // Helper to get provider display name from auth_type /** * Map an auth_type string to a human-readable provider name. * @param {string} authType - Auth type enum value (e.g. 'OAuth2Fastmail') * @returns {string|null} Provider display name, or null for Password auth */ function getAuthTypeDisplay(authType) { const providers = { 'OAuth2Fastmail': 'Fastmail', 'OAuth2Google': 'Google', 'OAuth2Microsoft': 'Microsoft', 'OAuth2Yahoo': 'Yahoo', }; return providers[authType] || null; } function buildAccountsListHtml(accounts) { if (accounts.length === 0) { return '

No email accounts configured

'; } return accounts.map(a => { const isOAuth = a.auth_type && a.auth_type !== 'Password'; const providerName = getAuthTypeDisplay(a.auth_type); const oauthBadge = providerName ? `${providerName}` : ''; const editBtn = isOAuth ? `` : ``; return `
${editBtn}
`; }).join(''); } async function openAccountsModal() { await loadAccounts(); const accountsList = buildAccountsListHtml(GoingsOn.state.emailAccounts); const content = `
${accountsList}
`; GoingsOn.ui.openModal('Email Accounts', content); } /** * Render the email-accounts management UI inline inside the settings * overlay's content panel. */ async function renderAccountsSection(container) { await loadAccounts(); const accountsList = buildAccountsListHtml(GoingsOn.state.emailAccounts); container.innerHTML = `

Email Accounts

Connect an email account via IMAP/SMTP to send and receive email from GoingsOn. Most providers need an app password rather than your normal password.

${accountsList}
`; } /** * Refresh whichever accounts UI is currently visible. Internal callbacks * (after add/edit/delete/OAuth) use this so the settings section refreshes * inline when settings is the active context, but the modal flow continues * to reopen the modal when invoked from the empty-state button. */ function refreshAccountsView() { const overlay = document.getElementById('settings-overlay'); const settingsOpen = overlay && !overlay.classList.contains('hidden'); const emailSectionActive = document.querySelector('.settings-nav-item.active')?.dataset.section === 'email'; if (settingsOpen && emailSectionActive) { const container = document.getElementById('settings-content'); if (container) { renderAccountsSection(container); GoingsOn.ui.closeModal(); return; } } openAccountsModal(); } async function openAddAccountModal() { // First, check what OAuth providers are available let oauthProviders = []; try { const response = await GoingsOn.api.oauth.listProviders(); oauthProviders = response.providers || []; } catch (e) { console.warn('Failed to load OAuth providers:', e); } // App-password (IMAP/SMTP) is the primary path. OAuth buttons only // appear once a provider is registered (post-launch); when present they // sit below the form as an alternative, not the recommended default. const hasOAuth = oauthProviders.length > 0; const imapIntro = `

Enter your account details below. Most providers (Gmail, Fastmail, iCloud, Yahoo, Outlook with 2-step verification) require an app password instead of your normal password — type your email address and GoingsOn shows the exact link to create one.

`; const oauthButtons = hasOAuth ? `
Or connect with OAuth
${oauthProviders.map(p => ` `).join('')}
Signs in through your provider; no app password needed.
` : ''; const formHtml = buildAccountFormHtml({ formId: 'email-account-form', idPrefix: 'acct', onSubmit: "GoingsOn.emails.createAccount(event)", values: {}, isEdit: false, submitLabel: 'Add Account', }); const content = `${imapIntro}${formHtml}${oauthButtons}`; GoingsOn.ui.openModal('Add Email Account', content); // Attach auto-detect for known providers after modal DOM is ready setTimeout(() => attachAutoDetect('acct'), 0); } async function createAccount(e) { e.preventDefault(); const form = e.target; const syncIntervalValue = form.sync_interval_minutes.value; const data = { accountName: form.account_name.value, emailAddress: form.email_address.value, imapServer: form.imap_server.value, imapPort: parseInt(form.imap_port.value), smtpServer: form.smtp_server.value, smtpPort: parseInt(form.smtp_port.value), username: form.username.value, password: form.password.value, useTls: form.use_tls.checked, archiveFolderName: form.archive_folder_name.value || 'Archive', syncIntervalMinutes: syncIntervalValue ? parseInt(syncIntervalValue) : null, }; await GoingsOn.ui.apiCall(GoingsOn.api.emailAccounts.create(data), { successMessage: 'Email account added!', errorMessage: 'Failed to add email account', closeModal: false, onSuccess: () => refreshAccountsView(), }); } /** * Open the edit form for an existing email account. * @param {string} id - Email account ID */ async function editAccount(id) { try { const account = await GoingsOn.api.emailAccounts.get(id); if (!account) { GoingsOn.ui.showToast('Account not found', 'error'); return; } const content = buildAccountFormHtml({ formId: 'edit-email-account-form', idPrefix: 'edit-acct', onSubmit: `GoingsOn.emails.updateAccount(event, '${escAttr(id)}')`, values: account, isEdit: true, submitLabel: 'Save Changes', }); GoingsOn.ui.openModal('Edit Email Account', content); } catch (err) { GoingsOn.ui.showToast(GoingsOn.utils.getErrorMessage(err, 'Failed to load account'), 'error'); } } async function updateAccount(e, id) { e.preventDefault(); const form = e.target; const syncIntervalValue = form.sync_interval_minutes.value; const data = { accountName: form.account_name.value, emailAddress: form.email_address.value, imapServer: form.imap_server.value, imapPort: parseInt(form.imap_port.value), smtpServer: form.smtp_server.value, smtpPort: parseInt(form.smtp_port.value), username: form.username.value, password: form.password.value || null, useTls: form.use_tls.checked, archiveFolderName: form.archive_folder_name.value || 'Archive', syncIntervalMinutes: syncIntervalValue ? parseInt(syncIntervalValue) : null, }; const signature = form.email_signature.value || null; const notifyNewEmails = form.notify_new_emails.checked; await GoingsOn.ui.apiCall(GoingsOn.api.emailAccounts.update(id, data), { successMessage: 'Email account updated!', errorMessage: 'Failed to update email account', closeModal: false, onSuccess: async () => { await GoingsOn.api.emailAccounts.updateSignature(id, signature); await GoingsOn.api.emailAccounts.updateNotify(id, notifyNewEmails); refreshAccountsView(); }, }); } /** * Delete an email account after confirmation. * @param {string} id - Email account ID */ async function deleteAccount(id) { if (!await GoingsOn.ui.confirmDelete('email account')) return; await GoingsOn.ui.apiCall(GoingsOn.api.emailAccounts.delete(id), { successMessage: 'Email account deleted!', errorMessage: 'Failed to delete email account', closeModal: false, onSuccess: () => refreshAccountsView(), }); } /** * Test IMAP/SMTP connectivity for an email account and show results. * @param {string} id - Email account ID */ async function testAccount(id) { GoingsOn.ui.showToast('Testing connection...', 'info'); try { const result = await GoingsOn.api.emailAccounts.test(id); // Build detailed result modal const imapStatus = result.imapSuccess ? 'IMAP OK' : 'IMAP Failed: ' + result.imapMessage; const smtpStatus = result.smtpSuccess ? 'SMTP OK' : 'SMTP Failed: ' + result.smtpMessage; const foldersHtml = result.availableFolders && result.availableFolders.length > 0 ? `
Available Folders:
${result.availableFolders.map(f => esc(f)).join('
')}
Use one of these folder names as the Archive Folder in account settings.
` : ''; const content = `
${imapStatus}
${smtpStatus}
${foldersHtml}
`; GoingsOn.ui.openModal('Connection Test Results', content); } catch (err) { GoingsOn.ui.showToast(GoingsOn.utils.getErrorMessage(err, 'Connection test failed'), 'error'); } } /** * Sync an email account (new or full) and show results. * @param {string} id - Email account ID * @param {boolean} [fullSync=false] - true for full re-sync, false for new-only */ async function syncAccount(id, fullSync = false) { GoingsOn.ui.showToast(fullSync ? 'Starting full sync...' : 'Starting sync...', 'info'); try { const result = await GoingsOn.api.emailAccounts.sync(id, fullSync); // Show detailed result in modal const content = `
Result: ${esc(result.message)}
INBOX: ${result.inboxFetched} found
Archive: ${result.archiveFetched} found
${result.debugInfo ? `
Debug Info:
${esc(result.debugInfo.split(' | ').join('\n'))}
` : ''}
`; GoingsOn.ui.openModal('Sync Results', content); // Also refresh emails if new ones were fetched if (result.emailsSaved > 0) { GoingsOn.emails.load(); } } catch (err) { GoingsOn.ui.showToast(GoingsOn.utils.getErrorMessage(err, 'Sync failed'), 'error', { action: { label: 'Retry', fn: () => syncAccount(id, fullSync) }, duration: 8000, }); } } // ============ OAuth Flow ============ // Store OAuth state during flow let pendingOAuthState = null; /** * Start the OAuth authorization flow for an email provider. * @param {string} providerId - OAuth provider ID (e.g. 'fastmail', 'google') */ async function startOAuth(providerId) { try { GoingsOn.ui.showToast('Starting OAuth flow...', 'info'); const result = await GoingsOn.api.oauth.start(providerId); // Store state for verification pendingOAuthState = { state: result.state, provider: result.provider, port: result.port, }; // Show waiting modal showOAuthWaitingModal(result.provider); // Open browser for authorization await window.__TAURI__.shell.open(result.authUrl); // Start listening for the callback listenForOAuthCallback(result.port); } catch (err) { GoingsOn.ui.showToast(GoingsOn.utils.getErrorMessage(err, 'Failed to start OAuth'), 'error'); } } function showOAuthWaitingModal(provider) { const providerNames = { 'fastmail': 'Fastmail', 'google': 'Google', 'microsoft': 'Microsoft', 'yahoo': 'Yahoo', }; const displayName = providerNames[provider] || provider; const content = `
Waiting for ${displayName} authorization...
A browser window should have opened. Please sign in and authorize the app.
`; GoingsOn.ui.openModal('Connecting Account', content); } function cancelOAuth() { pendingOAuthState = null; refreshAccountsView(); } async function listenForOAuthCallback(port) { // Poll for callback result const maxAttempts = 120; // 2 minutes let attempts = 0; const poll = async () => { if (!pendingOAuthState) return; // Cancelled attempts++; if (attempts > maxAttempts) { GoingsOn.ui.showToast('OAuth timeout - please try again', 'error'); pendingOAuthState = null; refreshAccountsView(); return; } try { // Check if there's a callback response available // The callback server writes to a temp location we can poll // Poll the local OAuth callback server. Expected to fail // repeatedly until the user completes the browser auth flow. const response = await fetch(`http://127.0.0.1:${port}/result`, { method: 'GET', mode: 'cors', }).catch(() => null); if (response && response.ok) { const data = await response.json(); if (data.code) { await completeOAuth(data.code, data.state); return; } else if (data.error) { GoingsOn.ui.showToast('OAuth error: ' + data.error, 'error'); pendingOAuthState = null; refreshAccountsView(); return; } } } catch (e) { // Ignore polling errors } // Continue polling setTimeout(poll, 1000); }; poll(); } async function completeOAuth(code, state) { if (!pendingOAuthState) { GoingsOn.ui.showToast('OAuth session expired', 'error'); return; } // Verify state matches if (state !== pendingOAuthState.state) { GoingsOn.ui.showToast('OAuth state mismatch - possible security issue', 'error'); pendingOAuthState = null; refreshAccountsView(); return; } try { GoingsOn.ui.showToast('Completing authorization...', 'info'); const result = await GoingsOn.api.oauth.complete({ code, state, }); pendingOAuthState = null; GoingsOn.ui.showToast(`Connected ${result.providerName} account: ${result.emailAddress}. Syncing...`, 'success'); refreshAccountsView(); // Auto-sync the newly connected account if (result.accountId) { syncAccount(result.accountId, false); } } catch (err) { pendingOAuthState = null; GoingsOn.ui.showToast(GoingsOn.utils.getErrorMessage(err, 'Failed to complete OAuth'), 'error'); refreshAccountsView(); } } /** * Re-authorize an existing OAuth email account. * @param {string} accountId - Email account ID to reconnect */ async function reconnectOAuth(accountId) { try { GoingsOn.ui.showToast('Starting reconnection...', 'info'); const result = await GoingsOn.api.oauth.reconnect(accountId); // Store state for verification pendingOAuthState = { state: result.state, provider: result.provider, port: result.port, accountId: accountId, // For updating existing account }; showOAuthWaitingModal(result.provider); await window.__TAURI__.shell.open(result.authUrl); listenForOAuthCallback(result.port); } catch (err) { GoingsOn.ui.showToast(GoingsOn.utils.getErrorMessage(err, 'Failed to start reconnection'), 'error'); } } // ============ Cache Helpers ============ function getAccountsCache() { return GoingsOn.state.emailAccounts; } function setAccountsCache(cache) { GoingsOn.state.set('emailAccounts', cache); } // ============ Extend GoingsOn.emails Namespace ============ Object.assign(GoingsOn.emails, { loadAccounts, openAccountsModal, renderAccountsSection, refreshAccountsView, openAddAccountModal, createAccount, editAccount, updateAccount, deleteAccount, testAccount, syncAccount, // OAuth startOAuth, cancelOAuth, completeOAuth, reconnectOAuth, // Cache getAccountsCache, setAccountsCache, }); })();