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
? `
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,
});
})();