import { Injectable, Logger } from '@nestjs/common'; import { CalendarEvent, CalendarProvider } from '../calendar.service'; /** * Exchange calendar provider — dispatches on exchangeMode ('graph' vs 'ews'). * * - 'graph': Uses @microsoft/microsoft-graph-client for Exchange Online / M365 * - 'ews': Uses ews-javascript-api for on-premise Exchange Server * * Both modes gracefully degrade: on auth failure, returns empty array and * surfaces a generic error (no credential details — Security V7 / T-05-13). */ @Injectable() export class ExchangeProvider implements CalendarProvider { private readonly logger = new Logger(ExchangeProvider.name); /** * Fetches events from Exchange, dispatching by exchangeMode. * D-08: source TYPE is configurable and attempted — widget must not crash. */ async fetchEvents( source: { url: string; username?: string; password?: string; exchangeMode?: string | null; id: string; color?: string | null; }, from: Date, to: Date, ): Promise { const mode = source.exchangeMode || 'graph'; try { if (mode === 'graph') { return await this.fetchViaGraph(source, from, to); } else { return await this.fetchViaEws(source, from, to); } } catch (error) { // Graceful degradation — T-05-13: no credential details in error this.logger.error( `Exchange (${mode}) fetch failed for source ${source.id}: ${(error as Error).message}`, ); return []; } } /** * Tests connection to Exchange. Returns false on any auth/network failure. */ async testConnection( source: { url: string; username?: string; password?: string; exchangeMode?: string | null; id: string; }, ): Promise { const mode = source.exchangeMode || 'graph'; try { if (mode === 'graph') { return await this.testGraphConnection(source); } else { return await this.testEwsConnection(source); } } catch { return false; } } /** * Fetches events via Microsoft Graph API (Exchange Online / M365). * Uses @microsoft/microsoft-graph-client with /me/calendarView. */ private async fetchViaGraph( source: { url: string; username?: string; password?: string; id: string; color?: string | null; }, from: Date, to: Date, ): Promise { // Dynamic import to avoid loading Graph SDK when not needed const { Client: GraphClient } = await import( '@microsoft/microsoft-graph-client' ); const client = GraphClient.init({ authProvider: (done: (error: any, token: string) => void) => { // Use the password as the access token (OAuth bearer token) // Users configure their OAuth token in the password field for Graph API done(null, source.password || ''); }, }); const result = await client .api('/me/calendarView') .query({ startDateTime: from.toISOString(), endDateTime: to.toISOString(), }) .select('id,subject,start,end,isAllDay,location,bodyPreview') .orderby('start/dateTime') .top(100) .get(); const events: CalendarEvent[] = []; if (result?.value) { for (const item of result.value) { events.push({ id: `${source.id}-${item.id}`, sourceId: source.id, title: item.subject || 'Untitled', start: new Date(item.start?.dateTime + 'Z'), end: new Date(item.end?.dateTime + 'Z'), allDay: item.isAllDay || false, location: item.location?.displayName || undefined, description: item.bodyPreview || undefined, color: source.color ?? undefined, }); } } return events; } /** * Fetches events via Exchange Web Services (on-premise Exchange). * Uses ews-javascript-api with FindAppointments over a CalendarView. * * Note: ews-javascript-api has no TypeScript definitions; * we use dynamic import + any casting for type safety. */ private async fetchViaEws( source: { url: string; username?: string; password?: string; id: string; color?: string | null; }, from: Date, to: Date, ): Promise { // Dynamic import — ews-javascript-api is JS-only, no .d.ts const ews: any = await import('ews-javascript-api'); const service = new ews.ExchangeService(ews.ExchangeVersion.Exchange2013); service.Url = new ews.Uri(source.url); service.Credentials = new ews.WebCredentials( source.username || '', source.password || '', ); // CalendarView constructor accepts JS Dates in ews-javascript-api const calendarView = new ews.CalendarView(from, to, 100); const findResults = await service.FindAppointments( ews.WellKnownFolderName.Calendar, calendarView, ); const events: CalendarEvent[] = []; for (const appointment of findResults.Items) { events.push({ id: `${source.id}-${appointment.Id?.UniqueId || String(Date.now())}`, sourceId: source.id, title: appointment.Subject || 'Untitled', start: appointment.Start ? new Date(String(appointment.Start)) : new Date(), end: appointment.End ? new Date(String(appointment.End)) : new Date(), allDay: appointment.IsAllDayEvent || false, location: appointment.Location || undefined, description: undefined, // Body requires separate load call color: source.color ?? undefined, }); } return events; } /** * Tests Graph API connection by requesting calendar list. */ private async testGraphConnection( source: { url: string; password?: string }, ): Promise { const { Client: GraphClient } = await import( '@microsoft/microsoft-graph-client' ); const client = GraphClient.init({ authProvider: (done: (error: any, token: string) => void) => { done(null, source.password || ''); }, }); const result = await client.api('/me/calendars').top(1).get(); return !!result?.value; } /** * Tests EWS connection by binding to the calendar folder. */ private async testEwsConnection( source: { url: string; username?: string; password?: string }, ): Promise { const ews: any = await import('ews-javascript-api'); const service = new ews.ExchangeService(ews.ExchangeVersion.Exchange2013); service.Url = new ews.Uri(source.url); service.Credentials = new ews.WebCredentials( source.username || '', source.password || '', ); await ews.Folder.Bind(service, ews.WellKnownFolderName.Calendar); return true; } }