| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404 |
- import type {
- AddDomainPayload,
- AdminMailboxAccessEntry,
- AdminResourceInventory,
- AdminUser,
- ApiToken,
- ApiTokenInput,
- Analytics,
- AuditLogEntry,
- DnsCredential,
- Domain,
- DomainPatchPayload,
- InboundMailboxDomain,
- InboundMailbox,
- InboundMailboxDeleteResult,
- InboundFolder,
- InboundMessage,
- InboundMessageFilters,
- MailboxPermissions,
- MailboxClientConfig,
- RuntimeConfig,
- SendEvent,
- SendEventFilters,
- SmtpCredential,
- SmtpCredentialInput,
- SmtpRelay,
- SmtpRelayInput,
- SystemEmailSettings,
- User,
- UserMergeOptions,
- UserMergePreview,
- UserMergeResult,
- UserRole,
- UserStatus,
- WebmailLogin,
- Webhook,
- WebhookDelivery,
- WebhookDeliveryFilters,
- WebhookPatchPayload,
- WebhookPayload
- } from '../types';
- interface RequestOptions extends RequestInit {
- data?: unknown;
- }
- async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {
- 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<RuntimeConfig>('/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
- }),
- deleteInboundMailbox: async (
- id: number,
- data: { targetMailboxId: number | null; confirmAddress: string }
- ) => {
- const response = await request<InboundMailboxDeleteResult | { result: InboundMailboxDeleteResult }>(
- `/api/inbound-mailboxes/${id}`,
- { method: 'DELETE', data }
- );
- return 'result' in response ? response.result : response;
- },
- 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`),
- createWebmailLogin: (mailboxId: number) =>
- request<{ webmailLogin: WebmailLogin }>(`/api/inbound-mailboxes/${mailboxId}/webmail-login`, {
- method: 'POST'
- }),
- 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<RuntimeConfig>) =>
- 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 }
- }),
- bulkUpdateAdminMailboxAccess: (data: {
- mailboxIds: number[];
- userIds: number[];
- operation: 'upsert' | 'remove';
- permissions?: MailboxPermissions;
- }) => request<{
- mailboxes: AdminMailboxAccessEntry[];
- summary: {
- mailboxCount: number;
- userCount: number;
- pairCount?: number;
- changedGrantCount: number;
- unchangedGrantCount?: number;
- skippedOwnerCount: number;
- revokedWebmailSessionCount?: number;
- };
- }>('/api/admin/inbound-mailboxes/access/bulk', {
- method: 'POST',
- data
- }),
- 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<UserMergeOptions>;
- 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<SystemEmailSettings>) =>
- 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' })
- };
- export function submitWebmailLogin(webmailLogin: WebmailLogin) {
- const form = document.createElement('form');
- form.hidden = true;
- form.method = 'POST';
- form.action = webmailLogin.action;
- form.target = '_self';
- form.acceptCharset = 'UTF-8';
- const fields = {
- mailhub_ticket: webmailLogin.ticket,
- _task: 'mail',
- _mbox: 'INBOX'
- };
- Object.entries(fields).forEach(([name, value]) => {
- const input = document.createElement('input');
- input.type = 'hidden';
- input.name = name;
- input.value = value;
- form.append(input);
- });
- document.body.append(form);
- try {
- if (typeof form.requestSubmit === 'function') form.requestSubmit();
- else form.submit();
- } finally {
- form.remove();
- }
- }
- 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;
- }
|