// REST client for the imap-copier control API. // All requests carry the session cookie; a 401 anywhere bounces to #/login. export type TLSMode = 'ssl' | 'starttls' | 'plain' export interface Endpoint { id: number role_label: string host: string port: number tls_mode: TLSMode } export interface Task { id: number name: string src_endpoint_id: number dst_endpoint_id: number status: string folder_mapping?: Record schedule_interval_seconds?: number broken?: boolean next_run_at?: string | null } export type TestStatus = 'pending' | 'ok' | 'fail' | string export interface Account { id: number src_login: string dst_login: string test_src_status: TestStatus test_dst_status: TestStatus status: string copied: number skipped: number errors: number last_error?: string folder_mapping?: Record excluded_folders?: string[] } export interface TaskDetail { task: Task accounts: Account[] } export class ApiError extends Error {} export async function api(path: string, opts: RequestInit = {}): Promise { const res = await fetch(path, { credentials: 'include', ...opts }) if (res.status === 401) { location.hash = '#/login' throw new ApiError('unauthorized') } if (!res.ok) { const body = await res.text() throw new ApiError(body || res.statusText) } const ct = res.headers.get('content-type') || '' if (ct.includes('application/json')) return res.json() as Promise return res.text() as unknown as T } const jsonBody = (body: unknown): RequestInit => ({ method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body), }) export const login = (user: string, pass: string) => api('/api/login', jsonBody({ user, pass })) export const logout = () => api('/api/logout', { method: 'POST' }) export const listEndpoints = () => api('/api/endpoints') export const createEndpoint = (body: { role_label: string; host: string; port: number; tls_mode: TLSMode }) => api<{ id: number }>('/api/endpoints', jsonBody(body)) export const updateEndpoint = ( id: number, body: { role_label: string; host: string; port: number; tls_mode: TLSMode }, ) => api(`/api/endpoints/${id}`, { ...jsonBody(body), method: 'PUT' }) export const deleteEndpoint = (id: number) => api(`/api/endpoints/${id}`, { method: 'DELETE' }) export const deleteTask = (id: number) => api(`/api/tasks/${id}`, { method: 'DELETE' }) export const deleteAccount = (taskId: number, accountId: number) => api(`/api/tasks/${taskId}/accounts/${accountId}`, { method: 'DELETE' }) export const cancelAccount = (taskId: number, accountId: number) => api(`/api/tasks/${taskId}/accounts/${accountId}/cancel`, { method: 'POST' }) export interface ProbeSide { ok: boolean folders?: string[] error?: string } export interface ProbeResult { src: ProbeSide dst: ProbeSide } export const probeFolders = ( taskId: number, creds: { src_login: string; src_pass: string; dst_login: string; dst_pass: string }, ) => api(`/api/tasks/${taskId}/probe`, jsonBody(creds)) export const probeAccountFolders = (taskId: number, accId: number) => api(`/api/tasks/${taskId}/accounts/${accId}/probe`, { method: 'POST' }) export const setAccountFolderMapping = ( taskId: number, accId: number, mapping: Record, excluded: string[], ) => api(`/api/tasks/${taskId}/accounts/${accId}/folder-mapping`, { ...jsonBody({ mapping, excluded }), method: 'PUT', }) export const listTasks = () => api('/api/tasks') export const getTask = (id: number) => api(`/api/tasks/${id}`) export const createTask = (body: { name: string src_endpoint_id: number dst_endpoint_id: number folder_mapping?: Record }) => api<{ id: number }>('/api/tasks', jsonBody(body)) export const createAccount = ( id: number, body: { src_login: string; src_pass: string; dst_login: string; dst_pass: string }, ) => api<{ id: number }>(`/api/tasks/${id}/accounts`, jsonBody(body)) export const testAccounts = (id: number) => api(`/api/tasks/${id}/test`, { method: 'POST' }) export const runTask = (id: number, accountIds?: number[]) => api(`/api/tasks/${id}/run`, accountIds?.length ? jsonBody({ account_ids: accountIds }) : { method: 'POST' }) export interface Run { id: number task_id: number status: string started_at: string finished_at: string | null total_copied: number total_skipped: number total_errors: number trigger: string } export const setTaskSchedule = (taskId: number, intervalSeconds: number) => api(`/api/tasks/${taskId}/schedule`, { ...jsonBody({ interval_seconds: intervalSeconds }), method: 'PUT' }) export const listRuns = (taskId: number) => api(`/api/tasks/${taskId}/runs`) export interface AccountError { id: number account_id: number run_id: number kind: string // folder | message | account folder: string message_ref: string error: string created_at: string } export const listAccountErrors = (taskId: number, accountId: number) => api(`/api/tasks/${taskId}/accounts/${accountId}/errors`) export const importCSV = (id: number, file: File) => { const fd = new FormData() fd.append('file', file) return api<{ imported: number }>(`/api/tasks/${id}/import`, { method: 'POST', body: fd }) } // A Kerio Connect export carries no domain, so the operator supplies it here; // the login and password apply to both sides of the migration. export const importKerioCSV = (id: number, file: File, domain: string) => { const fd = new FormData() fd.append('file', file) fd.append('format', 'kerio') fd.append('domain', domain) return api<{ imported: number }>(`/api/tasks/${id}/import`, { method: 'POST', body: fd }) }