kuamail/src/api/client.ts

106 lines
2.9 KiB
TypeScript

const BASE = '/api';
async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${BASE}${path}`, {
headers: { 'Content-Type': 'application/json', ...init?.headers },
...init,
});
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
return res.json() as Promise<T>;
}
export type Address = { name: string | null; email: string | null };
export type Folder = {
id: string;
imap_path: string;
display_name: string;
special_use: string | null;
unread_count: number;
total_count: number;
synced_at: string | null;
};
export type Thread = {
id: string;
subject: string;
participants: Address[];
message_count: number;
unread_count: number;
last_message_at: string;
snippet: string | null;
has_flagged: boolean;
has_attachment: boolean;
};
export type MessageSummary = {
id: string;
folder_id: string;
thread_id: string | null;
from_addr: Address;
to_addrs: Address[];
cc_addrs: Address[];
subject: string;
date_sent: string;
snippet: string;
flags: { seen: boolean; flagged: boolean; answered: boolean; draft: boolean };
attachments: Array<{ name: string; size: number; type: string }>;
};
export type MessageFull = MessageSummary & {
body_text: string | null;
body_html: string | null;
};
export type SyncRequest = {
account_id: string;
client_version: number;
mutations: Mutation[];
};
export type MutationTargetKind = 'thread' | 'message' | 'draft';
export type Mutation = {
id: string;
op: 'flag' | 'move' | 'delete' | 'send' | 'draft_save' | 'draft_delete';
target_kind: MutationTargetKind;
target_id: string;
payload: Record<string, unknown>;
};
export type SyncResponse = {
applied: string[];
rejected: Array<{ id: string; reason: string }>;
server_version: number;
changes: Array<{ type: 'message' | 'thread'; data: unknown }>;
};
export const api = {
accounts: {
list: () => apiFetch<{ accounts: Array<{ id: string; email: string }> }>('/accounts'),
},
folders: {
list: (accountId: string) =>
apiFetch<{ folders: Folder[] }>(`/accounts/${accountId}/folders`),
},
threads: {
list: (folderId: string, params?: { limit?: number; before?: string }) => {
const qs = new URLSearchParams();
if (params?.limit) qs.set('limit', String(params.limit));
if (params?.before) qs.set('before', params.before);
return apiFetch<{ threads: Thread[]; next_cursor: string | null }>(
`/folders/${folderId}/threads?${qs}`
);
},
},
messages: {
inThread: (threadId: string) =>
apiFetch<{ messages: MessageSummary[] }>(`/threads/${threadId}/messages`),
get: (id: string) => apiFetch<MessageFull>(`/messages/${id}`),
},
search: (accountId: string, q: string) =>
apiFetch<{ results: MessageSummary[] }>(`/search?account_id=${accountId}&q=${encodeURIComponent(q)}`),
sync: (body: SyncRequest) =>
apiFetch<SyncResponse>('/sync', { method: 'POST', body: JSON.stringify(body) }),
};