import { AppConfig, EmailMessage, EmailAttachment } from "@/types"; interface GraphTokenResponse { access_token: string; token_type: string; expires_in: number; } let cachedToken: { token: string; expiresAt: number } | null = null; export async function getAccessToken(config: AppConfig, userToken?: string): Promise { if (userToken) { return userToken; } if (cachedToken && Date.now() < cachedToken.expiresAt) { return cachedToken.token; } const url = `https://login.microsoftonline.com/${config.graph.tenantId}/oauth2/v2.0/token`; const params = new URLSearchParams({ client_id: config.graph.clientId, client_secret: config.graph.clientSecret, scope: "https://graph.microsoft.com/.default", grant_type: "client_credentials", }); const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 15000); let response: Response; try { response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: params.toString(), signal: controller.signal, }); } finally { clearTimeout(timeout); } if (!response.ok) { const error = await response.text(); throw new Error(`Failed to get access token: ${error}`); } const data: GraphTokenResponse = await response.json().catch(() => ({})); cachedToken = { token: data.access_token, expiresAt: Date.now() + (data.expires_in - 300) * 1000, }; return data.access_token; } async function graphRequest( token: string, endpoint: string, method: string = "GET", body?: unknown ): Promise { const url = endpoint.startsWith("http") ? endpoint : `https://graph.microsoft.com/v1.0${endpoint}`; const options: RequestInit = { method, headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, }; if (body) { options.body = JSON.stringify(body); } const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 30000); let response: Response; try { response = await fetch(url, { ...options, signal: controller.signal }); } finally { clearTimeout(timeout); } if (!response.ok) { const error = await response.text(); throw new Error(`Graph API error (${response.status}): ${error}`); } if (response.status === 204) { return null; } const text = await response.text(); return text ? JSON.parse(text) : null; } export async function fetchEmails( config: AppConfig, maxEmails: number = 50, folderId: string = "inbox", userToken?: string ): Promise { const token = await getAccessToken(config, userToken); const mailbox = config.graph.mailbox; const endpoint = `/users/${mailbox}/mailFolders/${folderId}/messages?$top=${maxEmails}&$select=id,subject,from,receivedDateTime,bodyPreview,hasAttachments,isRead,importance,categories,body&$orderby=receivedDateTime desc`; const data = await graphRequest(token, endpoint); if (!data.value || data.value.length === 0) { return []; } return data.value.map((msg: any): EmailMessage => ({ id: msg.id, subject: msg.subject || "(No Subject)", sender: msg.from?.emailAddress?.name || "Unknown", senderEmail: msg.from?.emailAddress?.address || "unknown@example.com", receivedDate: msg.receivedDateTime, bodyPreview: msg.bodyPreview || "", body: msg.body?.content || "", hasAttachments: msg.hasAttachments || false, attachments: [], isRead: msg.isRead || false, importance: msg.importance || "normal", categories: msg.categories || [], processed: false, })); } export async function fetchAttachments( config: AppConfig, emailId: string, userToken?: string ): Promise { const token = await getAccessToken(config, userToken); const mailbox = config.graph.mailbox; const endpoint = `/users/${mailbox}/messages/${emailId}/attachments`; const data = await graphRequest(token, endpoint); if (!data.value || data.value.length === 0) { return []; } return data.value.map((att: any): EmailAttachment => ({ id: att.id, name: att.name || "unknown", contentType: att.contentType || "application/octet-stream", size: att.size || 0, content: att.contentBytes ? new Uint8Array( atob(att.contentBytes) .split("") .map((c) => c.charCodeAt(0)) ) : undefined, })); } export async function markEmailAsRead( config: AppConfig, emailId: string, userToken?: string ): Promise { const token = await getAccessToken(config, userToken); const mailbox = config.graph.mailbox; await graphRequest(token, `/users/${mailbox}/messages/${emailId}`, "PATCH", { isRead: true, }); } export async function getEmail( config: AppConfig, emailId: string, userToken?: string ): Promise { const token = await getAccessToken(config, userToken); const mailbox = config.graph.mailbox; const endpoint = `/users/${mailbox}/messages/${emailId}?$select=id,subject,from,receivedDateTime,bodyPreview,hasAttachments,isRead,importance,categories,body`; const msg = await graphRequest(token, endpoint); return { id: msg.id, subject: msg.subject || "(No Subject)", sender: msg.from?.emailAddress?.name || "Unknown", senderEmail: msg.from?.emailAddress?.address || "unknown@example.com", receivedDate: msg.receivedDateTime, bodyPreview: msg.bodyPreview || "", body: msg.body?.content || "", hasAttachments: msg.hasAttachments || false, attachments: [], isRead: msg.isRead || false, importance: msg.importance || "normal", categories: msg.categories || [], processed: false, }; } export async function listFolders( config: AppConfig, userToken?: string ): Promise<{ id: string; name: string }[]> { const token = await getAccessToken(config, userToken); const mailbox = config.graph.mailbox; const data = await graphRequest( token, `/users/${mailbox}/mailFolders?$select=id,displayName` ); return (data.value || []).map((f: any) => ({ id: f.id, name: f.displayName, })); } export function validateConfig(config: AppConfig): string[] { const errors: string[] = []; if (!config.graph.clientId) errors.push("Graph client ID is required"); if (!config.graph.tenantId) errors.push("Graph tenant ID is required"); if (!config.llm.apiKey) errors.push("LLM API key is required"); return errors; }