| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| import type { TempProvider } from "./temp-providers"; |
| import { getSetting, setSetting } from "./db"; |
|
|
| export type CfAlias = { |
| address: string; |
| local: string; |
| createdAt: string | null; |
| forwardEnabled?: boolean; |
| forwardTo?: string[]; |
| id?: string | number; |
| }; |
| export type CfMessageSummary = { |
| uid: number; |
| subject: string | null; |
| from: string | null; |
| to: string | null; |
| date: string | null; |
| preview: string | null; |
| }; |
| export type CfMessageDetail = CfMessageSummary & { |
| recipientHint?: string | null; |
| bodyText?: string | null; |
| bodyHtml?: string | null; |
| }; |
| export type CfSendInput = { from: string; to: string[]; subject?: string; body?: string; html?: boolean }; |
| export type CfForwarding = { enabled: boolean; forwardTo: string[]; forwardedUids?: number[] }; |
|
|
| function localOf(addr: string): string { return (addr || "").split("@")[0]; } |
| function fullAddr(provider: TempProvider, local: string): string { |
| return local.includes("@") ? local : `${local}@${provider.domain}`; |
| } |
| function snippet(s: string | null | undefined, n = 140): string | null { |
| if (!s) return null; |
| const t = String(s).replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim(); |
| return t.length > n ? t.slice(0, n) : t; |
| } |
|
|
| |
| type PhpApiOptions = { method?: "GET" | "POST"; query?: Record<string, string | number | undefined>; body?: unknown; form?: Record<string, string | number | boolean | undefined> }; |
| async function phpApi<T = any>(provider: TempProvider, action: string, options: PhpApiOptions = {}): Promise<T> { |
| const url = new URL(provider.endpoint); |
| url.searchParams.set("action", action); |
| for (const [k, v] of Object.entries(options.query ?? {})) if (v !== undefined) url.searchParams.set(k, String(v)); |
| const headers: Record<string, string> = { "X-Admin-Password": provider.password }; |
| const init: RequestInit = { method: options.method ?? "GET", headers }; |
| if (options.form !== undefined) { |
| const params = new URLSearchParams(); |
| for (const [k, v] of Object.entries(options.form)) if (v !== undefined) params.set(k, String(v)); |
| headers["Content-Type"] = "application/x-www-form-urlencoded"; |
| init.body = params.toString(); init.method = "POST"; |
| } else if (options.body !== undefined) { |
| headers["Content-Type"] = "application/json"; init.body = JSON.stringify(options.body); |
| } |
| const res = await fetch(url, init); |
| const text = await res.text(); |
| const data = text ? JSON.parse(text) : null; |
| if (!res.ok) throw new Error((data && (data.error || data.message)) || `temp-mail HTTP ${res.status}`); |
| return data as T; |
| } |
|
|
| |
| |
| function cfNames(provider: TempProvider): string[] { |
| try { const v = JSON.parse(getSetting(`cf.addresses.${provider.id}`) || "[]"); return Array.isArray(v) ? v : []; } catch { return []; } |
| } |
| function cfAddName(provider: TempProvider, local: string): void { |
| const list = cfNames(provider); |
| if (!list.includes(local)) { list.push(local); setSetting(`cf.addresses.${provider.id}`, JSON.stringify(list)); } |
| } |
| function cfRemoveName(provider: TempProvider, local: string): void { |
| setSetting(`cf.addresses.${provider.id}`, JSON.stringify(cfNames(provider).filter((n) => n !== local))); |
| } |
|
|
| |
| async function cfMint(provider: TempProvider, local: string): Promise<{ address: string; jwt: string; addressId?: string }> { |
| const res = await fetch(provider.endpoint.replace(/\/+$/, "") + "/admin/new_address", { |
| method: "POST", |
| headers: { "x-admin-auth": provider.password, "content-type": "application/json" }, |
| body: JSON.stringify({ name: local, domain: provider.domain, enablePrefix: false }) |
| }); |
| const text = await res.text(); |
| let j: any = null; try { j = JSON.parse(text); } catch { } |
| if (!res.ok || !j?.jwt) throw new Error(j?.error || j?.message || `cf new_address HTTP ${res.status}`); |
| return { address: j.address || fullAddr(provider, local), jwt: j.jwt, addressId: j.address_id }; |
| } |
|
|
| async function cfReadParsed(provider: TempProvider, local: string, limit = 50): Promise<any[]> { |
| const { jwt } = await cfMint(provider, local); |
| const res = await fetch(provider.endpoint.replace(/\/+$/, "") + `/api/parsed_mails?limit=${limit}`, { |
| headers: { authorization: `Bearer ${jwt}` } |
| }); |
| const text = await res.text(); |
| let j: any = null; try { j = JSON.parse(text); } catch { } |
| if (!res.ok) throw new Error(j?.error || j?.message || `cf parsed_mails HTTP ${res.status}`); |
| return Array.isArray(j) ? j : (j?.results ?? j?.mails ?? []); |
| } |
|
|
| |
| function cfMap(m: any, alias: string): CfMessageSummary { |
| return { |
| uid: Number(m.id ?? 0), |
| subject: m.subject ?? null, |
| from: m.sender ?? m.source ?? null, |
| to: m.address ?? alias, |
| date: m.created_at ?? null, |
| preview: snippet(m.text) |
| }; |
| } |
|
|
| |
| export function cfDomain(provider: TempProvider): string { return provider.domain; } |
|
|
| export async function cfStatus(provider: TempProvider): Promise<any> { |
| if (provider.type === "cf") { |
| |
| await cfMint(provider, "healthcheck"); |
| return { domain: provider.domain }; |
| } |
| return phpApi(provider, "external_status"); |
| } |
|
|
| |
| async function cfAdminList(provider: TempProvider): Promise<CfAlias[]> { |
| const base = provider.endpoint.replace(/\/+$/, ""); |
| const out: CfAlias[] = []; |
| const seen = new Set<string>(); |
| const limit = 100; |
| for (let page = 0, offset = 0; page < 100; page++, offset += limit) { |
| const res = await fetch(`${base}/admin/address?limit=${limit}&offset=${offset}`, { |
| headers: { "x-admin-auth": provider.password } |
| }); |
| if (!res.ok) throw new Error(`cf admin/address HTTP ${res.status}`); |
| const text = await res.text(); |
| let j: any = null; |
| try { j = JSON.parse(text); } catch { } |
| const results: any[] = Array.isArray(j?.results) ? j.results : []; |
| for (const r of results) { |
| const address = String(r.address || r.name || ""); |
| if (!address) continue; |
| |
| if (provider.domain && (address.split("@")[1] || "").toLowerCase() !== provider.domain.toLowerCase()) continue; |
| if (seen.has(address)) continue; |
| seen.add(address); |
| |
| out.push({ address, local: String(r.name ?? localOf(address)), createdAt: r.created_at ?? null }); |
| } |
| if (results.length < limit) break; |
| } |
| return out; |
| } |
|
|
| export async function cfListAliases(provider: TempProvider): Promise<CfAlias[]> { |
| if (provider.type === "cf") { |
| |
| try { |
| return await cfAdminList(provider); |
| } catch { |
| return cfNames(provider).map((n) => ({ address: fullAddr(provider, n), local: n, createdAt: null })); |
| } |
| } |
| const data = await phpApi<{ aliases?: CfAlias[] }>(provider, "external_aliases"); |
| return data.aliases ?? []; |
| } |
|
|
| export async function cfInbox(provider: TempProvider, alias: string): Promise<CfMessageSummary[]> { |
| if (provider.type === "cf") { |
| const local = localOf(alias); |
| cfAddName(provider, local); |
| const mails = await cfReadParsed(provider, local); |
| return mails.map((m) => cfMap(m, alias)); |
| } |
| const data = await phpApi<{ messages?: CfMessageSummary[] }>(provider, "external_inbox", { query: { alias } }); |
| return data.messages ?? []; |
| } |
|
|
| |
| |
| export async function cfInboxRich(provider: TempProvider, alias: string, limit: number, offset: number): Promise<CfMessageDetail[]> { |
| if (provider.type === "cf") { |
| const mails = await cfReadParsed(provider, localOf(alias)); |
| return mails.slice(offset, offset + limit).map((m) => { |
| const html: string | null = m.html ?? null; |
| const body: string = m.text ?? ""; |
| return { ...cfMap(m, alias), bodyText: body || null, bodyHtml: html }; |
| }); |
| } |
| const summaries = (await cfInbox(provider, alias)).slice(offset, offset + limit); |
| const out: CfMessageDetail[] = []; |
| for (const s of summaries) { |
| try { out.push(await cfMessage(provider, alias, s.uid)); } |
| catch { out.push({ ...s, bodyText: s.preview ?? null, bodyHtml: null }); } |
| } |
| return out; |
| } |
|
|
| export async function cfSent(provider: TempProvider, alias: string): Promise<CfMessageSummary[]> { |
| if (provider.type === "cf") return []; |
| const data = await phpApi<{ messages?: CfMessageSummary[] }>(provider, "external_sent", { query: { alias } }); |
| return data.messages ?? []; |
| } |
|
|
| export async function cfSearch( |
| provider: TempProvider, |
| alias: string, |
| query: { keyword?: string; from?: string; subject?: string; limit?: number } |
| ): Promise<CfMessageSummary[]> { |
| const messages = await cfInbox(provider, alias); |
| const kw = query.keyword?.trim().toLowerCase(); |
| const fromQ = query.from?.trim().toLowerCase(); |
| const subjQ = query.subject?.trim().toLowerCase(); |
| return messages.filter((m) => { |
| if (kw) { const hay = `${m.subject ?? ""}\n${m.from ?? ""}\n${m.preview ?? ""}`.toLowerCase(); if (!hay.includes(kw)) return false; } |
| if (fromQ && !(m.from ?? "").toLowerCase().includes(fromQ)) return false; |
| if (subjQ && !(m.subject ?? "").toLowerCase().includes(subjQ)) return false; |
| return true; |
| }).slice(0, query.limit ?? 50); |
| } |
|
|
| export async function cfMessage(provider: TempProvider, alias: string, uid: number): Promise<CfMessageDetail> { |
| if (provider.type === "cf") { |
| const mails = await cfReadParsed(provider, localOf(alias), 100); |
| const m = mails.find((x) => Number(x.id) === Number(uid)) ?? {}; |
| const body: string = m.text ?? ""; |
| const html: string | null = m.html ?? null; |
| return { ...cfMap(m, alias), bodyText: body || null, bodyHtml: html }; |
| } |
| const data = await phpApi<{ message: CfMessageDetail }>(provider, "external_message", { query: { alias, uid } }); |
| return data.message; |
| } |
|
|
| export async function cfCreateAlias(provider: TempProvider, local: string): Promise<CfAlias> { |
| if (provider.type === "cf") { |
| const r = await cfMint(provider, local); |
| cfAddName(provider, localOf(r.address)); |
| return { address: r.address, local: localOf(r.address), createdAt: null, id: r.addressId }; |
| } |
| const data = await phpApi<{ alias?: CfAlias } & Partial<CfAlias>>(provider, "external_create_alias", { method: "POST", body: { local } }); |
| return data.alias ?? (data as CfAlias); |
| } |
|
|
| export async function cfDeleteAlias(provider: TempProvider, local: string): Promise<void> { |
| if (provider.type === "cf") { |
| |
| cfRemoveName(provider, localOf(local)); |
| return; |
| } |
| await phpApi(provider, "external_delete_alias", { method: "POST", body: { local } }); |
| } |
|
|
| export async function cfSend(provider: TempProvider, input: CfSendInput): Promise<any> { |
| if (provider.type === "cf") { |
| |
| const res = await fetch(provider.endpoint.replace(/\/+$/, "") + "/admin/send_mail", { |
| method: "POST", |
| headers: { "x-admin-auth": provider.password, "content-type": "application/json" }, |
| body: JSON.stringify({ |
| from_mail: input.from, |
| to_mail: input.to.join(","), |
| subject: input.subject ?? "", |
| content: input.body ?? "", |
| is_html: Boolean(input.html) |
| }) |
| }); |
| const text = await res.text(); |
| let j: any = null; try { j = JSON.parse(text); } catch { } |
| if (!res.ok) throw new Error(j?.error || j?.message || `cf send_mail HTTP ${res.status}`); |
| return j ?? { status: "ok" }; |
| } |
| return phpApi(provider, "external_send", { |
| method: "POST", |
| body: { from: input.from, to: input.to, subject: input.subject ?? "", body: input.body ?? "", format: input.html ? "html" : "text" } |
| }); |
| } |
|
|
| export async function cfGlobalForwarding(provider: TempProvider): Promise<CfForwarding> { |
| if (provider.type === "cf") return { enabled: false, forwardTo: [] }; |
| const data = await phpApi<{ forwarding: CfForwarding }>(provider, "external_global_forwarding"); |
| return data.forwarding; |
| } |
|
|
| export async function cfUpdateAliasForwarding(provider: TempProvider, address: string, enabled: boolean, forwardTo: string[]): Promise<CfAlias[]> { |
| if (provider.type === "cf") throw new Error("cloudflare_temp_email 壳不支持转发设置"); |
| const data = await phpApi<{ aliases?: CfAlias[] }>(provider, "external_update_forwarding", { |
| form: { address, enabled: enabled ? 1 : 0, forwardTo: forwardTo.join(",") } |
| }); |
| return data.aliases ?? []; |
| } |
|
|
| export async function cfUpdateGlobalForwarding(provider: TempProvider, enabled: boolean, forwardTo: string[]): Promise<CfForwarding> { |
| if (provider.type === "cf") throw new Error("cloudflare_temp_email 壳不支持转发设置"); |
| const data = await phpApi<{ forwarding: CfForwarding }>(provider, "external_update_global_forwarding", { |
| form: { enabled: enabled ? 1 : 0, forwardTo: forwardTo.join(",") } |
| }); |
| return data.forwarding; |
| } |
|
|