import type { AddDomainPayload, AdminMailboxAccessEntry, AdminResourceInventory, AdminUser, ApiToken, ApiTokenInput, Analytics, AuditLogEntry, DnsCredential, Domain, DomainPatchPayload, InboundMailboxDomain, InboundMailbox, InboundFolder, InboundMessage, InboundMessageFilters, MailboxPermissions, MailboxClientConfig, RuntimeConfig, SendEvent, SendEventFilters, SmtpCredential, SmtpCredentialInput, SmtpRelay, SmtpRelayInput, SystemEmailSettings, User, UserMergeOptions, UserMergePreview, UserMergeResult, UserRole, UserStatus, Webhook, WebhookDelivery, WebhookDeliveryFilters, WebhookPatchPayload, WebhookPayload } from '../types'; interface RequestOptions extends RequestInit { data?: unknown; } async function request(path: string, options: RequestOptions = {}): Promise { const headers = new Headers(options.headers); if (options.data !== undefined && !headers.has('Content-Type')) { headers.set('Content-Type', 'application/json'); } const response = await send(path, { method: options.method || 'GET', headers, body: options.data === undefined ? options.body : JSON.stringify(options.data) }); const text = response.text; const payload = text ? JSON.parse(text) : {}; if (response.status < 200 || response.status >= 300) { throw new Error(payload.error || payload.message || requestFailedMessage(response.status)); } return payload as T; } function send( path: string, options: { method: string; headers: Headers; body?: BodyInit | null } ): Promise<{ status: number; text: string }> { if (typeof globalThis.fetch === 'function') { return globalThis.fetch(path, { method: options.method, headers: options.headers, body: options.body }).then(async (response) => ({ status: response.status, text: await response.text() })); } return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open(options.method, path, true); options.headers.forEach((value, key) => xhr.setRequestHeader(key, value)); xhr.onload = () => resolve({ status: xhr.status, text: xhr.responseText || '' }); xhr.onerror = () => reject(new Error(networkFailedMessage())); xhr.send((options.body || null) as XMLHttpRequestBodyInit | null); }); } function requestFailedMessage(status: number) { return currentLocale().startsWith('en') ? `Request failed: ${status}` : `请求失败:${status}`; } function networkFailedMessage() { return currentLocale().startsWith('en') ? 'Network request failed' : '网络请求失败'; } function currentLocale() { return document.documentElement.lang || window.localStorage.getItem('mailhub.locale') || 'zh-CN'; } export const api = { me: () => request<{ user: User }>('/api/me'), config: () => request('/api/config'), domains: () => request<{ domains: Domain[] }>('/api/domains'), events: (filters: SendEventFilters = {}) => { const query = queryString(filters); return request<{ events: SendEvent[]; total: number; page: number; pageSize: number }>( `/api/events${query ? `?${query}` : ''}` ); }, event: (id: number) => request<{ event: SendEvent | null }>(`/api/events/${id}`), inboundMailboxes: (scope: boolean | 'owned' | 'effective' | 'all' = false) => { const query = typeof scope === 'boolean' ? (scope ? '?all=true' : '') : `?scope=${encodeURIComponent(scope)}`; return request<{ mailboxes: InboundMailbox[] }>(`/api/inbound-mailboxes${query}`); }, inboundMailboxDomains: () => request<{ domains: InboundMailboxDomain[] }>('/api/inbound-mailbox-domains'), createInboundMailbox: (data: { address: string; displayName?: string; password: string; aliases?: string | string[]; forwardTo?: string | string[]; keepForwarded?: boolean; quotaMb?: number | string | null; }) => request<{ mailbox: InboundMailbox; clientConfig?: MailboxClientConfig }>('/api/inbound-mailboxes', { method: 'POST', data }), updateInboundMailbox: (id: number, data: Partial<{ displayName: string; password: string; aliases: string | string[]; forwardTo: string | string[]; keepForwarded: boolean; quotaMb: number | string | null; status: string; }>) => request<{ mailbox: InboundMailbox; clientConfig?: MailboxClientConfig }>(`/api/inbound-mailboxes/${id}`, { method: 'PATCH', data }), inboundMessages: (filters: InboundMessageFilters | number | null = {}) => { const normalized = typeof filters === 'number' ? { mailboxId: filters } : (filters || {}); const query = queryString(normalized); return request<{ messages: InboundMessage[]; total: number; page: number; pageSize: number }>( `/api/inbound-messages${query ? `?${query}` : ''}` ); }, inboundFolders: (mailboxId: number) => request<{ folders: InboundFolder[] }>(`/api/inbound-mailboxes/${mailboxId}/folders`), inboundMessage: (id: number) => request<{ message: InboundMessage | null }>(`/api/inbound-messages/${id}`), markInboundMessageRead: (id: number, read = true) => request<{ message: InboundMessage | null }>(`/api/inbound-messages/${id}`, { method: 'PATCH', data: { read } }), analytics: (days = 7) => request<{ analytics: Analytics }>(`/api/analytics?days=${days}`), smtpCredential: () => request<{ credential: SmtpCredential | null }>('/api/smtp-credential'), saveSmtpCredential: (data: SmtpCredentialInput) => request<{ credential: SmtpCredential }>('/api/smtp-credential', { method: 'PUT', data }), smtpCredentials: () => request<{ credentials: SmtpCredential[] }>('/api/smtp-credentials'), smtpCredentialDetail: (id: number) => request<{ credential: SmtpCredential }>(`/api/smtp-credentials/${id}`), saveSmtpLoginCredential: (data: SmtpCredentialInput, id?: number) => request<{ credential: SmtpCredential }>(id ? `/api/smtp-credentials/${id}` : '/api/smtp-credentials', { method: id ? 'PATCH' : 'POST', data }), deleteSmtpCredential: (id: number) => request<{ deleted: boolean }>(`/api/smtp-credentials/${id}`, { method: 'DELETE' }), smtpRelays: () => request<{ relays: SmtpRelay[] }>('/api/smtp-relays'), smtpRelay: (id: number) => request<{ relay: SmtpRelay }>(`/api/smtp-relays/${id}`), saveSmtpRelay: (data: SmtpRelayInput, id?: number) => request<{ relay: SmtpRelay }>(id ? `/api/smtp-relays/${id}` : '/api/smtp-relays', { method: id ? 'PATCH' : 'POST', data }), deleteSmtpRelay: (id: number) => request<{ deleted: boolean }>(`/api/smtp-relays/${id}`, { method: 'DELETE' }), dnsCredentials: () => request<{ credentials: DnsCredential[] }>('/api/dns-credentials'), saveDnsCredential: (data: object, id?: number) => request<{ credential: DnsCredential }>(id ? `/api/dns-credentials/${id}` : '/api/dns-credentials', { method: id ? 'PATCH' : 'POST', data }), testDnsCredential: (id: number) => request<{ ok: boolean; detail?: string; provider?: string; error?: string }>(`/api/dns-credentials/${id}/test`, { method: 'POST' }), deleteDnsCredential: (id: number) => request<{ deleted: boolean }>(`/api/dns-credentials/${id}`, { method: 'DELETE' }), apiTokens: () => request<{ tokens: ApiToken[] }>('/api/api-tokens'), createApiToken: (data: ApiTokenInput & { name: string }) => request<{ token: ApiToken }>('/api/api-tokens', { method: 'POST', data }), updateApiToken: (id: number, data: ApiTokenInput) => request<{ token: ApiToken }>(`/api/api-tokens/${id}`, { method: 'PATCH', data }), rotateApiToken: (id: number) => request<{ token: ApiToken }>(`/api/api-tokens/${id}/rotate`, { method: 'POST' }), deleteApiToken: (id: number) => request<{ deleted: boolean; revoked: boolean; token?: ApiToken | null }>(`/api/api-tokens/${id}`, { method: 'DELETE' }), createDomain: (data: AddDomainPayload) => request<{ domain: Domain }>('/api/domains', { method: 'POST', data }), patchDomain: (id: number, data: DomainPatchPayload) => request<{ domain: Domain }>(`/api/domains/${id}`, { method: 'PATCH', data }), checkDomain: (id: number) => request<{ domain: Domain }>(`/api/domains/${id}/check`, { method: 'POST' }), applyDns: (id: number) => request<{ domain: Domain; apply?: Domain['status']['apply'] }>(`/api/domains/${id}/apply-dns`, { method: 'POST' }), rotateDkim: (id: number, selector?: string) => request<{ domain: Domain }>(`/api/domains/${id}/rotate-dkim`, { method: 'POST', data: { selector } }), sendTest: ( id: number, data: { from?: string; to: string; subject?: string; text?: string; html?: string; tracking?: boolean | { opens?: boolean; clicks?: boolean }; smtpRelayId?: number | string | null; } ) => request<{ queued: boolean }>(`/api/domains/${id}/test-send`, { method: 'POST', data }), deleteDomain: (id: number) => request<{ deleted: boolean }>(`/api/domains/${id}`, { method: 'DELETE' }), adminSettings: () => request<{ settings: RuntimeConfig }>('/api/admin/settings'), saveAdminSettings: (data: Partial) => request<{ settings: RuntimeConfig }>('/api/admin/settings', { method: 'PATCH', data }), adminUsers: () => request<{ users: AdminUser[] }>('/api/admin/users'), adminMailboxAccess: async () => { const result = await request<{ mailboxes?: AdminMailboxAccessEntry[]; entries?: AdminMailboxAccessEntry[]; }>('/api/admin/mailbox-access'); return { mailboxes: result.mailboxes || result.entries || [] }; }, saveAdminMailboxAccess: ( id: number, grants: Array<{ userId: number } & MailboxPermissions> ) => request<{ mailbox: AdminMailboxAccessEntry }>(`/api/admin/inbound-mailboxes/${id}/access`, { method: 'PUT', data: { grants } }), updateAdminUser: (id: number, data: { role?: UserRole; status?: UserStatus; password?: string }) => request<{ user: AdminUser }>(`/api/admin/users/${id}`, { method: 'PATCH', data }), approveAdminUser: (id: number) => request<{ user: AdminUser }>(`/api/admin/users/${id}/approve`, { method: 'POST' }), resendAdminVerification: (id: number) => request<{ verificationEmailSent?: boolean; message: string; result?: SystemMailActionResult; }>(`/api/admin/users/${id}/resend-verification`, { method: 'POST' }), sendAdminPasswordReset: (id: number) => request<{ result: SystemMailActionResult }>(`/api/admin/users/${id}/password-reset`, { method: 'POST' }), setAdminTemporaryPassword: (id: number, password: string) => request<{ user: AdminUser }>(`/api/admin/users/${id}/temporary-password`, { method: 'POST', data: { password } }), adminResources: () => request<{ inventory: AdminResourceInventory }>('/api/admin/resources'), transferAdminDomain: ( id: number, data: { targetUserId: number; dnsCredentialMode?: 'domain_only' | 'with_dns_credential' | 'clear_dns_credential' } ) => request<{ domain: Domain }>(`/api/admin/resources/domains/${id}/transfer`, { method: 'POST', data }), transferAdminDnsCredential: (id: number, data: { targetUserId: number }) => request<{ credential: DnsCredential }>(`/api/admin/resources/dns-credentials/${id}/transfer`, { method: 'POST', data }), transferAdminApiTokens: (data: { tokenIds: number[]; targetUserId: number }) => request<{ tokens: ApiToken[] }>('/api/admin/resources/api-tokens/transfer', { method: 'POST', data }), previewUserMerge: (data: { sourceUserId: number; targetUserId: number }) => request<{ preview: UserMergePreview }>('/api/admin/migrations/user-merge/preview', { method: 'POST', data }), executeUserMerge: (data: { sourceUserId: number; targetUserId: number; options: Partial; confirmation: string; }) => request<{ result: UserMergeResult }>('/api/admin/migrations/user-merge/execute', { method: 'POST', data }), adminSystemEmail: () => request<{ settings: SystemEmailSettings }>('/api/admin/system-email'), saveAdminSystemEmail: (data: Partial) => request<{ settings: SystemEmailSettings }>('/api/admin/system-email', { method: 'PATCH', data }), testAdminSystemEmail: (to?: string) => request<{ result: SystemMailActionResult }>('/api/admin/system-email/test', { method: 'POST', data: to ? { to } : {} }), adminAuditLogs: (query = '') => request<{ logs: AuditLogEntry[] }>(`/api/admin/audit-logs${query ? `?${query}` : ''}`), resendVerification: (email: string) => request<{ message: string }>('/api/auth/resend-verification', { method: 'POST', data: { email } }), forgotPassword: (email: string) => request<{ message: string }>('/api/auth/forgot-password', { method: 'POST', data: { email } }), resetPassword: (token: string, password: string) => request<{ message: string }>('/api/auth/reset-password', { method: 'POST', data: { token, password } }), logout: () => request<{ ok: boolean }>('/api/logout', { method: 'POST' }), webhooks: (domainId?: number | null, mailboxId?: number) => { const params = new URLSearchParams(); if (domainId === null) params.set('domainId', 'null'); else if (domainId !== undefined) params.set('domainId', String(domainId)); if (mailboxId !== undefined) params.set('mailboxId', String(mailboxId)); const query = params.toString(); return request<{ webhooks: Webhook[] }>(`/api/webhooks${query ? `?${query}` : ''}`); }, createWebhook: (data: WebhookPayload) => request<{ webhook: Webhook }>('/api/webhooks', { method: 'POST', data }), updateWebhook: (id: number, data: WebhookPatchPayload) => request<{ webhook: Webhook }>(`/api/webhooks/${id}`, { method: 'PATCH', data }), deleteWebhook: (id: number) => request<{ deleted: boolean }>(`/api/webhooks/${id}`, { method: 'DELETE' }), rotateWebhookSecret: (id: number) => request<{ webhook: Webhook }>(`/api/webhooks/${id}/rotate-secret`, { method: 'POST' }), testWebhook: (id: number) => request<{ delivery: WebhookDelivery }>(`/api/webhooks/${id}/test`, { method: 'POST' }), webhookDeliveries: (filters: WebhookDeliveryFilters = {}) => { const params = new URLSearchParams(); if (filters.status) params.set('status', String(filters.status)); if (filters.webhookId != null) params.set('webhookId', String(filters.webhookId)); if (filters.eventType) params.set('eventType', String(filters.eventType)); if (filters.limit != null) params.set('limit', String(filters.limit)); const query = params.toString(); return request<{ deliveries: WebhookDelivery[] }>( `/api/webhook-deliveries${query ? `?${query}` : ''}` ); }, replayWebhookDelivery: (id: number) => request<{ delivery: WebhookDelivery }>(`/api/webhook-deliveries/${id}/replay`, { method: 'POST' }) }; function queryString(values: object) { const params = new URLSearchParams(); Object.entries(values).forEach(([key, value]) => { if (value === undefined || value === null || value === '') return; params.set(key, typeof value === 'boolean' ? String(value) : String(value)); }); return params.toString(); } interface SystemMailActionResult { ok: boolean; message: string; queueId?: string; }