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