/** * 🏢 Microsoft Graph Email & SharePoint Harvester * Bruger Graph API til at søge i TDC Outlook og SharePoint */ import { logger } from '../../utils/logger.js'; export interface GraphSearchResult { id: string; title: string; url: string; summary: string; type: 'email' | 'document' | 'site' | 'list'; from?: string; received?: string; modified?: string; } export class MicrosoftGraphAdapter { private accessToken: string | null = null; private graphBase = 'https://graph.microsoft.com/v1.0'; constructor(accessToken?: string) { this.accessToken = accessToken || process.env.MS_GRAPH_TOKEN || null; } setAccessToken(token: string) { this.accessToken = token; logger.info('🔐 Microsoft Graph access token set'); } async isAvailable(): Promise { if (!this.accessToken) return false; try { const response = await fetch(`${this.graphBase}/me`, { headers: { 'Authorization': `Bearer ${this.accessToken}` } }); return response.ok; } catch { return false; } } async searchEmails(query: string, limit = 25): Promise { if (!this.accessToken) { logger.warn('⚠️ No Graph access token - use setAccessToken()'); return []; } try { // Search messages const response = await fetch( `${this.graphBase}/me/messages?$search="${encodeURIComponent(query)}"&$top=${limit}&$select=id,subject,from,receivedDateTime,bodyPreview,webLink`, { headers: { 'Authorization': `Bearer ${this.accessToken}`, 'ConsistencyLevel': 'eventual' } } ); if (!response.ok) { logger.error(`Graph email search failed: ${response.status}`); return []; } const data = await response.json(); return (data.value || []).map((msg: any) => ({ id: msg.id, title: msg.subject || '(No subject)', url: msg.webLink || `https://outlook.office365.com/mail/deeplink/read/${msg.id}`, summary: msg.bodyPreview?.slice(0, 300) || '', type: 'email' as const, from: msg.from?.emailAddress?.address || 'Unknown', received: msg.receivedDateTime })); } catch (err) { logger.error('Graph email search error:', err); return []; } } async searchSharePoint(query: string, limit = 25): Promise { if (!this.accessToken) return []; try { const searchBody = { requests: [{ entityTypes: ['driveItem', 'listItem', 'site'], query: { queryString: query }, from: 0, size: limit }] }; const response = await fetch(`${this.graphBase}/search/query`, { method: 'POST', headers: { 'Authorization': `Bearer ${this.accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify(searchBody) }); if (!response.ok) { logger.error(`Graph SharePoint search failed: ${response.status}`); return []; } const data = await response.json(); const results: GraphSearchResult[] = []; for (const resultSet of data.value || []) { for (const container of resultSet.hitsContainers || []) { for (const hit of container.hits || []) { const resource = hit.resource || {}; results.push({ id: resource.id || hit.hitId, title: resource.name || resource.displayName || query, url: resource.webUrl || '', summary: hit.summary?.slice(0, 300) || '', type: this.detectType(resource['@odata.type']), modified: resource.lastModifiedDateTime }); } } } return results; } catch (err) { logger.error('Graph SharePoint search error:', err); return []; } } async searchAll(query: string, limit = 20): Promise { const [emails, docs] = await Promise.all([ this.searchEmails(query, limit), this.searchSharePoint(query, limit) ]); return [...emails, ...docs]; } async getSites(): Promise<{ id: string; name: string; url: string }[]> { if (!this.accessToken) return []; try { const response = await fetch( `${this.graphBase}/sites?search=*&$top=50`, { headers: { 'Authorization': `Bearer ${this.accessToken}` } } ); if (!response.ok) return []; const data = await response.json(); return (data.value || []).map((site: any) => ({ id: site.id, name: site.displayName, url: site.webUrl })); } catch { return []; } } private detectType(odataType: string): 'email' | 'document' | 'site' | 'list' { if (!odataType) return 'document'; if (odataType.includes('site')) return 'site'; if (odataType.includes('listItem')) return 'list'; return 'document'; } } // Singleton instance export const graphAdapter = new MicrosoftGraphAdapter();