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',
submitAct: 'emails.createAccount',
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: true, // TLS is always enforced server-side (implicit IMAP TLS + mandatory SMTP STARTTLS); no opt-out.
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',
submitAct: 'emails.updateAccount', submitId: 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: true, // TLS is always enforced server-side (implicit IMAP TLS + mandatory SMTP STARTTLS); no opt-out.
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
// imapMessage/smtpMessage are server-returned strings — escape before innerHTML.
const imapStatus = result.imapSuccess ? 'IMAP OK' : 'IMAP Failed: ' + esc(result.imapMessage);
const smtpStatus = result.smtpSuccess ? 'SMTP OK' : 'SMTP Failed: ' + esc(result.smtpMessage);
const foldersHtml = result.availableFolders && result.availableFolders.length > 0
? `
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) {
// A first full sync pulls thousands of messages over minutes. Show a
// persistent progress modal (reuses the OAuth-waiting pattern) so the
// app doesn't look frozen; the modal also blocks the Sync buttons behind
// it, preventing the double-click-queues-a-second-sync problem.
showSyncProgressModal(fullSync
? 'Full sync in progress. This can take a few minutes for the first run.'
: 'Syncing new messages...');
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) {
// Dismiss the progress modal before surfacing the error toast.
GoingsOn.ui.closeModal();
GoingsOn.ui.showToast(GoingsOn.utils.getErrorMessage(err, 'Sync failed'), 'error', {
action: { label: 'Retry', fn: () => syncAccount(id, fullSync) },
duration: 8000,
});
}
}
/**
* Persistent spinner modal shown while an email sync runs. Replaced by the
* Sync Results modal on success, or dismissed on error. Mirrors
* showOAuthWaitingModal.
* @param {string} message - Status line describing the in-progress sync
*/
function showSyncProgressModal(message) {
const content = `
Syncing email...
${esc(message)}
`;
GoingsOn.ui.openModal('Email Sync', content);
}
// ============ 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 {
// Poll the local callback server over IPC. Stays "pending"
// until the user completes the browser auth flow.
const data = await GoingsOn.api.oauth.pollResult(port);
if (data.status === 'success' && data.code) {
await completeOAuth(data.code, data.state);
return;
} else if (data.status === 'error') {
GoingsOn.ui.showToast('OAuth error: ' + (data.error || 'unknown'), '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,
showSyncProgressModal,
// OAuth
startOAuth,
cancelOAuth,
completeOAuth,
reconnectOAuth,
// Cache
getAccountsCache,
setAccountsCache,
});
})();