2026-06-16 10:10:21 +08:00
|
|
|
import type { User, AdminUser, AdminOverview, Domain, Mailbox, Alias, MailFolder, Attachment, MailLabel, MailMessage, DNSRecord, DNSCheckResult, ListResponse, SendPayload, Contact, MailRule, MailRuleCondition, MailRuleAction, BlockedSender, MailStats, MailboxApplyOptions, MailTemplate, SystemSettings, SystemSettingsPayload, PublicSettings, LoginPayload, LoginResponse, RegisterPayload } from "./api-types"
|
|
|
|
|
export * from "./api-types"
|
2026-06-14 01:07:48 +08:00
|
|
|
|
2026-06-15 17:37:40 +08:00
|
|
|
const REQUEST_TIMEOUT_MS = 15_000
|
2026-06-16 22:28:04 +08:00
|
|
|
const MAIL_DELIVERY_TIMEOUT_MS = 60_000
|
2026-06-15 17:37:40 +08:00
|
|
|
|
2026-06-16 22:28:04 +08:00
|
|
|
async function request<T>(path: string, init: RequestInit & { timeoutMs?: number } = {}): Promise<T> {
|
|
|
|
|
const { timeoutMs, ...requestInit } = init
|
2026-06-15 17:37:40 +08:00
|
|
|
const controller = new AbortController()
|
2026-06-16 22:28:04 +08:00
|
|
|
const timeout = window.setTimeout(() => controller.abort(), timeoutMs || REQUEST_TIMEOUT_MS)
|
|
|
|
|
const externalSignal = requestInit.signal
|
2026-06-15 17:37:40 +08:00
|
|
|
if (externalSignal) {
|
|
|
|
|
if (externalSignal.aborted) controller.abort()
|
|
|
|
|
else externalSignal.addEventListener("abort", () => controller.abort(), { once: true })
|
|
|
|
|
}
|
|
|
|
|
try {
|
2026-06-16 22:28:04 +08:00
|
|
|
const res = await fetch(path, { credentials: "include", headers: { "Content-Type": "application/json", ...(requestInit.headers || {}) }, ...requestInit, signal: controller.signal })
|
2026-06-15 17:37:40 +08:00
|
|
|
if (!res.ok) {
|
|
|
|
|
let message = `${res.status} ${res.statusText}`
|
|
|
|
|
try { const body = await res.json(); message = body.error || message } catch {}
|
|
|
|
|
throw new Error(message)
|
|
|
|
|
}
|
|
|
|
|
return res.json() as Promise<T>
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (error instanceof DOMException && error.name === "AbortError") {
|
|
|
|
|
throw new Error("请求超时,请检查后端服务是否正常")
|
|
|
|
|
}
|
|
|
|
|
throw error instanceof Error ? error : new Error("网络请求失败")
|
|
|
|
|
} finally {
|
|
|
|
|
window.clearTimeout(timeout)
|
2026-06-14 01:07:48 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const api = {
|
2026-06-15 00:37:43 +08:00
|
|
|
publicSettings: () => request<PublicSettings>("/api/public/settings"),
|
2026-06-15 23:57:56 +08:00
|
|
|
register: (payload: RegisterPayload) => request<{ user: User }>("/api/auth/register", { method: "POST", body: JSON.stringify(payload) }),
|
2026-06-15 00:37:43 +08:00
|
|
|
login: (payload: LoginPayload) => request<LoginResponse>("/api/auth/login", { method: "POST", body: JSON.stringify(payload) }),
|
2026-06-14 01:07:48 +08:00
|
|
|
logout: () => request<{ ok: boolean }>("/api/auth/logout", { method: "POST" }),
|
|
|
|
|
me: () => request<{ user: User }>("/api/me"),
|
|
|
|
|
updateProfile: (payload: { displayName: string }) => request<{ user: User }>("/api/me/profile", { method: "POST", body: JSON.stringify(payload) }),
|
|
|
|
|
changePassword: (payload: { currentPassword: string; newPassword: string }) => request<{ ok: boolean }>("/api/me/password", { method: "POST", body: JSON.stringify(payload) }),
|
2026-06-15 00:37:43 +08:00
|
|
|
setupTwoFactor: () => request<{ secret: string; otpauthUrl: string }>("/api/me/2fa/setup", { method: "POST" }),
|
|
|
|
|
enableTwoFactor: (code: string) => request<{ user: User }>("/api/me/2fa/enable", { method: "POST", body: JSON.stringify({ code }) }),
|
|
|
|
|
disableTwoFactor: (code: string) => request<{ user: User }>("/api/me/2fa/disable", { method: "POST", body: JSON.stringify({ code }) }),
|
2026-06-14 01:07:48 +08:00
|
|
|
contacts: () => request<ListResponse<Contact>>("/api/me/contacts"),
|
|
|
|
|
createContact: (payload: { name: string; email: string; note: string }) => request<Contact>("/api/me/contacts", { method: "POST", body: JSON.stringify(payload) }),
|
|
|
|
|
deleteContact: (id: string) => request<{ ok: boolean }>(`/api/me/contacts/${id}`, { method: "DELETE" }),
|
|
|
|
|
rules: () => request<ListResponse<MailRule>>("/api/me/rules"),
|
2026-06-15 21:50:44 +08:00
|
|
|
createRule: (payload: { mailboxId: string; name: string; matchMode: "all" | "any"; conditions: MailRuleCondition[]; actions: MailRuleAction[]; applyToExisting: boolean; stopProcessing: boolean; enabled: boolean }) => request<MailRule>("/api/me/rules", { method: "POST", body: JSON.stringify(payload) }),
|
2026-06-14 01:07:48 +08:00
|
|
|
deleteRule: (id: string) => request<{ ok: boolean }>(`/api/me/rules/${id}`, { method: "DELETE" }),
|
|
|
|
|
blockedSenders: () => request<ListResponse<BlockedSender>>("/api/me/blocked-senders"),
|
|
|
|
|
createBlockedSender: (payload: { mailboxId: string; email: string; reason: string }) => request<BlockedSender>("/api/me/blocked-senders", { method: "POST", body: JSON.stringify(payload) }),
|
|
|
|
|
deleteBlockedSender: (id: string) => request<{ ok: boolean }>(`/api/me/blocked-senders/${id}`, { method: "DELETE" }),
|
|
|
|
|
mailStats: (mailboxId?: string) => request<MailStats>(`/api/me/stats${mailboxId ? `?mailboxId=${encodeURIComponent(mailboxId)}` : ""}`),
|
|
|
|
|
cleanupMail: (payload: { mailboxId: string; target: "empty-trash" | "empty-spam" | "archive-read-inbox" }) => request<{ ok: boolean; affected: number }>("/api/me/cleanup", { method: "POST", body: JSON.stringify(payload) }),
|
2026-06-16 00:51:41 +08:00
|
|
|
mailboxApplyOptions: () => request<MailboxApplyOptions>("/api/me/mailbox-apply-options"),
|
|
|
|
|
applyMailbox: (payload: { domainId: string; localPart: string; displayName: string }) => request<Mailbox>("/api/me/mailboxes/apply", { method: "POST", body: JSON.stringify(payload) }),
|
2026-06-15 00:37:43 +08:00
|
|
|
adminOverview: () => request<AdminOverview>("/api/admin/overview"),
|
|
|
|
|
users: () => request<ListResponse<AdminUser>>("/api/admin/users"),
|
|
|
|
|
createUser: (payload: { email: string; displayName: string; role: "admin" | "user"; password: string; disabled: boolean }) => request<AdminUser>("/api/admin/users", { method: "POST", body: JSON.stringify(payload) }),
|
|
|
|
|
updateUser: (id: string, payload: { displayName: string; role: "admin" | "user"; disabled: boolean }) => request<AdminUser>(`/api/admin/users/${id}`, { method: "POST", body: JSON.stringify(payload) }),
|
|
|
|
|
resetUserPassword: (id: string, password: string) => request<{ ok: boolean }>(`/api/admin/users/${id}/password`, { method: "POST", body: JSON.stringify({ password }) }),
|
|
|
|
|
deleteUser: (id: string) => request<{ ok: boolean }>(`/api/admin/users/${id}`, { method: "DELETE" }),
|
2026-06-14 01:07:48 +08:00
|
|
|
domains: () => request<ListResponse<Domain>>("/api/admin/domains"),
|
|
|
|
|
createDomain: (name: string) => request<Domain>("/api/admin/domains", { method: "POST", body: JSON.stringify({ name }) }),
|
2026-06-15 00:37:43 +08:00
|
|
|
updateDomain: (id: string, payload: { status: string }) => request<Domain>(`/api/admin/domains/${id}`, { method: "POST", body: JSON.stringify(payload) }),
|
|
|
|
|
deleteDomain: (id: string) => request<{ ok: boolean }>(`/api/admin/domains/${id}`, { method: "DELETE" }),
|
2026-06-14 01:07:48 +08:00
|
|
|
mailboxes: () => request<ListResponse<Mailbox>>("/api/admin/mailboxes"),
|
2026-06-15 00:37:43 +08:00
|
|
|
createMailbox: (payload: { domainId: string; localPart: string; displayName: string; password: string; quotaMb: number; role: "admin" | "user"; ownerEmail?: string; userId?: string }) => request<Mailbox>("/api/admin/mailboxes", { method: "POST", body: JSON.stringify(payload) }),
|
|
|
|
|
updateMailbox: (id: string, payload: { userId: string; displayName: string; quotaMb: number; status: string }) => request<Mailbox>(`/api/admin/mailboxes/${id}`, { method: "POST", body: JSON.stringify(payload) }),
|
|
|
|
|
deleteMailbox: (id: string) => request<{ ok: boolean }>(`/api/admin/mailboxes/${id}`, { method: "DELETE" }),
|
2026-06-14 01:07:48 +08:00
|
|
|
aliases: () => request<ListResponse<Alias>>("/api/admin/aliases"),
|
|
|
|
|
createAlias: (payload: { domainId: string; source: string; destination: string; enabled: boolean }) => request<Alias>("/api/admin/aliases", { method: "POST", body: JSON.stringify(payload) }),
|
2026-06-15 00:37:43 +08:00
|
|
|
updateAlias: (id: string, payload: { source: string; destination: string; enabled: boolean }) => request<Alias>(`/api/admin/aliases/${id}`, { method: "POST", body: JSON.stringify(payload) }),
|
|
|
|
|
deleteAlias: (id: string) => request<{ ok: boolean }>(`/api/admin/aliases/${id}`, { method: "DELETE" }),
|
|
|
|
|
adminMessages: (params: { mailboxId?: string; folder?: string; q?: string; cursor?: string } = {}) => {
|
|
|
|
|
const query = new URLSearchParams()
|
|
|
|
|
if (params.mailboxId) query.set("mailboxId", params.mailboxId)
|
|
|
|
|
if (params.folder) query.set("folder", params.folder)
|
|
|
|
|
if (params.q) query.set("q", params.q)
|
|
|
|
|
if (params.cursor) query.set("cursor", params.cursor)
|
|
|
|
|
const suffix = query.toString()
|
|
|
|
|
return request<ListResponse<MailMessage>>(`/api/admin/messages${suffix ? `?${suffix}` : ""}`)
|
|
|
|
|
},
|
|
|
|
|
adminMessage: (id: string) => request<MailMessage>(`/api/admin/messages/${id}`),
|
|
|
|
|
systemSettings: () => request<SystemSettings>("/api/admin/settings"),
|
|
|
|
|
updateSystemSettings: (payload: SystemSettingsPayload) => request<SystemSettings>("/api/admin/settings", { method: "POST", body: JSON.stringify(payload) }),
|
2026-06-16 22:28:04 +08:00
|
|
|
testSmtp: (to: string) => request<{ ok: boolean }>("/api/admin/settings/test-smtp", { method: "POST", body: JSON.stringify({ to }), timeoutMs: MAIL_DELIVERY_TIMEOUT_MS }),
|
2026-06-15 00:37:43 +08:00
|
|
|
mailTemplates: () => request<ListResponse<MailTemplate>>("/api/admin/mail-templates"),
|
|
|
|
|
updateMailTemplate: (key: string, payload: { subject: string; bodyText: string; bodyHtml: string }) => request<MailTemplate>(`/api/admin/mail-templates/${encodeURIComponent(key)}`, { method: "POST", body: JSON.stringify(payload) }),
|
|
|
|
|
resetMailTemplate: (key: string) => request<MailTemplate>(`/api/admin/mail-templates/${encodeURIComponent(key)}/reset`, { method: "POST" }),
|
2026-06-14 01:07:48 +08:00
|
|
|
dnsRecords: (domainId: string) => request<{ items: DNSRecord[] }>(`/api/admin/domains/${domainId}/dns-records`),
|
|
|
|
|
checkDns: (domainId: string) => request<DNSCheckResult>(`/api/admin/domains/${domainId}/check-dns`, { method: "POST" }),
|
|
|
|
|
myMailboxes: () => request<ListResponse<Mailbox>>("/api/mail/mailboxes"),
|
|
|
|
|
folders: (mailboxId?: string) => request<ListResponse<MailFolder>>(`/api/mail/folders${mailboxId ? `?mailboxId=${encodeURIComponent(mailboxId)}` : ""}`),
|
2026-06-15 17:37:40 +08:00
|
|
|
labels: (mailboxId?: string) => request<ListResponse<MailLabel>>(`/api/mail/labels${mailboxId ? `?mailboxId=${encodeURIComponent(mailboxId)}` : ""}`),
|
|
|
|
|
createLabel: (payload: { mailboxId?: string; name: string; color?: string }) => {
|
|
|
|
|
const query = payload.mailboxId ? `?mailboxId=${encodeURIComponent(payload.mailboxId)}` : ""
|
|
|
|
|
return request<MailLabel>(`/api/mail/labels${query}`, { method: "POST", body: JSON.stringify({ name: payload.name, color: payload.color || "" }) })
|
|
|
|
|
},
|
2026-06-14 01:07:48 +08:00
|
|
|
messages: (folder: string, q = "", cursor = "", mailboxId?: string) => {
|
|
|
|
|
const params = new URLSearchParams({ folder, q, cursor })
|
|
|
|
|
if (mailboxId) params.set("mailboxId", mailboxId)
|
|
|
|
|
return request<ListResponse<MailMessage>>(`/api/mail/messages?${params.toString()}`)
|
|
|
|
|
},
|
2026-06-15 17:37:40 +08:00
|
|
|
labelMessages: (labelId: string, q = "", cursor = "", mailboxId?: string) => {
|
|
|
|
|
const params = new URLSearchParams({ labelId, q, cursor })
|
|
|
|
|
if (mailboxId) params.set("mailboxId", mailboxId)
|
|
|
|
|
return request<ListResponse<MailMessage>>(`/api/mail/messages?${params.toString()}`)
|
|
|
|
|
},
|
|
|
|
|
starredMessages: (q = "", cursor = "", mailboxId?: string) => {
|
|
|
|
|
const params = new URLSearchParams({ q, cursor })
|
|
|
|
|
if (mailboxId) params.set("mailboxId", mailboxId)
|
|
|
|
|
return request<ListResponse<MailMessage>>(`/api/mail/starred?${params.toString()}`)
|
|
|
|
|
},
|
2026-06-15 21:50:44 +08:00
|
|
|
message: (id: string, options: { markRead?: boolean } = {}) => request<MailMessage>(`/api/mail/messages/${id}${options.markRead === false ? "?markRead=0" : ""}`),
|
2026-06-16 22:28:04 +08:00
|
|
|
send: (payload: SendPayload) => request<MailMessage>("/api/mail/send", { method: "POST", body: JSON.stringify(payload), timeoutMs: MAIL_DELIVERY_TIMEOUT_MS }),
|
2026-06-14 01:07:48 +08:00
|
|
|
markRead: (id: string, read: boolean) => request<{ ok: boolean }>(`/api/mail/messages/${id}/mark-read`, { method: "POST", body: JSON.stringify({ read }) }),
|
|
|
|
|
star: (id: string, starred: boolean) => request<{ ok: boolean }>(`/api/mail/messages/${id}/star`, { method: "POST", body: JSON.stringify({ starred }) }),
|
2026-06-15 17:37:40 +08:00
|
|
|
addLabel: (id: string, payload: { name: string; color?: string }) => request<{ labels: MailLabel[] }>(`/api/mail/messages/${id}/labels`, { method: "POST", body: JSON.stringify(payload) }),
|
|
|
|
|
removeLabel: (id: string, labelID: string) => request<{ labels: MailLabel[] }>(`/api/mail/messages/${id}/labels/${labelID}`, { method: "DELETE" }),
|
2026-06-14 01:07:48 +08:00
|
|
|
move: (id: string, folder: string) => request<{ ok: boolean }>(`/api/mail/messages/${id}/move`, { method: "POST", body: JSON.stringify({ folder }) }),
|
|
|
|
|
delete: (id: string) => request<{ ok: boolean }>(`/api/mail/messages/${id}`, { method: "DELETE" }),
|
|
|
|
|
}
|
2026-06-15 00:37:43 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|