import { apiClient, API_BASE_URL } from "./client"; async function fetchApi( endpoint: string, options: { method?: string; data?: unknown } = {} ): Promise { const { method = "GET", data } = options; try { const response = await apiClient({ url: endpoint, method, data, }); if (response.data.success && response.data.data !== undefined) { return response.data.data as T; } return response.data as T; } catch (error: unknown) { if (error && typeof error === "object" && "response" in error) { const axiosError = error as { response?: { data?: { message?: string }; status?: number }; }; throw new Error( axiosError.response?.data?.message || `HTTP ${axiosError.response?.status}` ); } throw new Error("Unknown error"); } } export interface UserMailAccount { id: number; displayName: string; email: string; protocol: "imap" | "pop3"; host: string; port: number; useTls: boolean; username: string; status: string; createdAt: string; updatedAt: string; } export interface CreateUserMailAccountDto { displayName: string; email: string; protocol: "imap" | "pop3"; host: string; port: number; useTls: boolean; username: string; password: string; } export interface ReceivedMail { id: string; messageId: string; from: string; to: string; subject: string; date: string; preview: string; isRead: boolean; hasAttachments: boolean; } export interface MailDetail extends ReceivedMail { htmlBody: string; textBody: string; cc?: string; bcc?: string; attachments: Array<{ filename: string; contentType: string; size: number }>; } export async function getUserMailAccounts(): Promise { return fetchApi("/user-mail/accounts"); } export async function createUserMailAccount( dto: CreateUserMailAccountDto ): Promise { return fetchApi("/user-mail/accounts", { method: "POST", data: dto, }); } export async function updateUserMailAccount( id: number, dto: Partial ): Promise { return fetchApi(`/user-mail/accounts/${id}`, { method: "PUT", data: dto, }); } export async function deleteUserMailAccount(id: number): Promise { return fetchApi(`/user-mail/accounts/${id}`, { method: "DELETE" }); } export async function testUserMailAccount( id: number ): Promise<{ success: boolean; message: string }> { return fetchApi<{ success: boolean; message: string }>( `/user-mail/accounts/${id}/test`, { method: "POST" } ); } export async function testUserMailConnectionDirect(dto: { protocol: string; host: string; port: number; useTls: boolean; username: string; password: string; }): Promise<{ success: boolean; message: string }> { return fetchApi<{ success: boolean; message: string }>( `/user-mail/test-connection`, { method: "POST", data: dto } ); } export async function getUserMails( accountId: number, limit: number = 50 ): Promise { return fetchApi( `/user-mail/accounts/${accountId}/mails?limit=${limit}` ); } export async function getUserMailDetail( accountId: number, seqno: number ): Promise { return fetchApi( `/user-mail/accounts/${accountId}/mails/${seqno}` ); } export async function markUserMailAsRead( accountId: number, seqno: number ): Promise { return fetchApi( `/user-mail/accounts/${accountId}/mails/${seqno}/mark-read`, { method: "POST" } ); } export async function deleteUserMail( accountId: number, seqno: number ): Promise { return fetchApi(`/user-mail/accounts/${accountId}/mails/${seqno}`, { method: "DELETE", }); } export function streamUserMails( accountId: number, limit: number = 20, before: number | null = null, onMail: (mail: ReceivedMail) => void, onDone: () => void, onError: (err: string) => void ): () => void { const token = typeof window !== "undefined" ? localStorage.getItem("authToken") : null; const url = before ? `${API_BASE_URL}/user-mail/accounts/${accountId}/mails/stream?limit=${limit}&before=${before}` : `${API_BASE_URL}/user-mail/accounts/${accountId}/mails/stream?limit=${limit}`; const controller = new AbortController(); (async () => { try { const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` }, signal: controller.signal, }); if (!res.ok || !res.body) { onError("스트리밍 연결 실패"); return; } const reader = res.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\n"); buffer = lines.pop() || ""; let currentEvent = "message"; for (const line of lines) { if (line.startsWith("event: ")) { currentEvent = line.slice(7).trim(); if (currentEvent === "done") onDone(); } else if (line.startsWith("data: ")) { if (currentEvent === "message") { try { const mail = JSON.parse(line.slice(6)); if (mail?.id) onMail(mail); } catch {} } else if (currentEvent === "error") { try { const { message } = JSON.parse(line.slice(6)); onError(message || "스트리밍 오류"); } catch {} } currentEvent = "message"; // 리셋 } } } } catch (e: any) { if (e.name !== "AbortError") onError(e.message || "연결 오류"); } })(); return () => controller.abort(); } export interface MailFolder { path: string; name: string; unseen: number; } export interface SendMailDto { to: string; cc?: string; subject: string; html: string; text?: string; inReplyTo?: string; references?: string; } export interface MailAttachment { partId: string; filename: string; contentType: string; size: number; } export async function getUserMailFolders(accountId: number): Promise { return fetchApi(`/user-mail/accounts/${accountId}/folders`); } export async function moveUserMail( accountId: number, seqno: number, targetFolder: string ): Promise<{ success: boolean; message: string }> { return fetchApi<{ success: boolean; message: string }>( `/user-mail/accounts/${accountId}/mails/${seqno}/move`, { method: 'POST', data: { targetFolder } } ); } export async function sendUserMail( accountId: number, dto: SendMailDto ): Promise<{ success: boolean; message: string }> { return fetchApi<{ success: boolean; message: string }>( `/user-mail/accounts/${accountId}/send`, { method: 'POST', data: dto } ); } export async function getUserMailAttachments( accountId: number, seqno: number, folder: string = 'INBOX' ): Promise { return fetchApi( `/user-mail/accounts/${accountId}/mails/${seqno}/attachments?folder=${encodeURIComponent(folder)}` ); } export async function downloadAttachment( accountId: number, seqno: number, partId: string, filename: string, folder: string = 'INBOX', onProgress?: (percent: number) => void, totalSize?: number ): Promise { const token = typeof window !== 'undefined' ? localStorage.getItem('authToken') : null; const url = `${API_BASE_URL}/user-mail/accounts/${accountId}/mails/${seqno}/attachment/${encodeURIComponent(partId)}?folder=${encodeURIComponent(folder)}`; const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } }); if (!res.ok) throw new Error(`다운로드 실패: ${res.status}`); const contentLength = totalSize || parseInt(res.headers.get('content-length') || '0'); if (contentLength && onProgress && res.body) { const reader = res.body.getReader(); const chunks: Uint8Array[] = []; let received = 0; while (true) { const { done, value } = await reader.read(); if (done) break; chunks.push(value); received += value.length; onProgress(Math.min(99, Math.round((received / contentLength) * 100))); } onProgress(100); const blob = new Blob(chunks); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(() => URL.revokeObjectURL(a.href), 1000); } else { const blob = await res.blob(); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(() => URL.revokeObjectURL(a.href), 1000); } } export function streamFolderMails( accountId: number, folder: string, limit: number = 20, before: number | null = null, onMail: (mail: ReceivedMail) => void, onDone: () => void, onError: (err: string) => void ): () => void { const token = typeof window !== 'undefined' ? localStorage.getItem('authToken') : null; const encodedFolder = encodeURIComponent(folder); const url = before ? `${API_BASE_URL}/user-mail/accounts/${accountId}/folders/${encodedFolder}/mails/stream?limit=${limit}&before=${before}` : `${API_BASE_URL}/user-mail/accounts/${accountId}/folders/${encodedFolder}/mails/stream?limit=${limit}`; const controller = new AbortController(); (async () => { try { const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` }, signal: controller.signal, }); if (!res.ok || !res.body) { onError('스트리밍 연결 실패'); return; } const reader = res.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() || ''; let currentEvent = 'message'; for (const line of lines) { if (line.startsWith('event: ')) { currentEvent = line.slice(7).trim(); if (currentEvent === 'done') onDone(); } else if (line.startsWith('data: ')) { if (currentEvent === 'message') { try { const mail = JSON.parse(line.slice(6)); if (mail?.id) onMail(mail); } catch {} } else if (currentEvent === 'error') { try { const { message } = JSON.parse(line.slice(6)); onError(message || '오류'); } catch {} } currentEvent = 'message'; } } } } catch (e: any) { if (e.name !== 'AbortError') onError(e.message || '연결 오류'); } })(); return () => controller.abort(); }