import { Appointment, Attendee, CalendarView, ConflictResolutionMode, DateTime, DeleteMode, ExchangeService, ExchangeVersion, FolderId, FolderView, ItemId, LegacyFreeBusyStatus, MessageBody, PropertySet, SendInvitationsMode, SendInvitationsOrCancellationsMode, Uri, WebCredentials, WellKnownFolderName, } from "ews-javascript-api"; import { symmetricDecrypt } from "@calcom/lib/crypto"; // Probably don't need // import { CALENDAR_INTEGRATIONS_TYPES } from "@calcom/lib/integrations/calendar/constants/generals"; import logger from "@calcom/lib/logger"; import type { Calendar, CalendarEvent, EventBusyDate, IntegrationCalendar, NewCalendarEventType, } from "@calcom/types/Calendar"; import type { CredentialPayload } from "@calcom/types/Credential"; export default class ExchangeCalendarService implements Calendar { private url = ""; private integrationName = ""; private log: typeof logger; private readonly exchangeVersion: ExchangeVersion; private credentials: Record; constructor(credential: CredentialPayload) { this.integrationName = "exchange2013_calendar"; this.log = logger.getChildLogger({ prefix: [`[[lib] ${this.integrationName}`] }); const decryptedCredential = JSON.parse( symmetricDecrypt(credential.key?.toString() || "", process.env.CALENDSO_ENCRYPTION_KEY || "") ); const username = decryptedCredential.username; const url = decryptedCredential.url; const password = decryptedCredential.password; this.url = url; this.credentials = { username, password, }; this.exchangeVersion = ExchangeVersion.Exchange2013; } async createEvent(event: CalendarEvent): Promise { try { const appointment = new Appointment(this.getExchangeService()); // service instance of ExchangeService appointment.Subject = event.title; appointment.Start = DateTime.Parse(event.startTime); // moment string appointment.End = DateTime.Parse(event.endTime); // moment string appointment.Location = event.location || "Location not defined!"; appointment.Body = new MessageBody(event.description || ""); // you can not use any special character or escape the content for (let i = 0; i < event.attendees.length; i++) { appointment.RequiredAttendees.Add(new Attendee(event.attendees[i].email)); } await appointment.Save(SendInvitationsMode.SendToAllAndSaveCopy); return { uid: appointment.Id.UniqueId, id: appointment.Id.UniqueId, password: "", type: "", url: "", additionalInfo: {}, }; } catch (reason) { this.log.error(reason); throw reason; } } // eslint-disable-next-line @typescript-eslint/no-explicit-any async updateEvent(uid: string, event: CalendarEvent): Promise { try { const appointment = await Appointment.Bind( this.getExchangeService(), new ItemId(uid), new PropertySet() ); appointment.Subject = event.title; appointment.Start = DateTime.Parse(event.startTime); // moment string appointment.End = DateTime.Parse(event.endTime); // moment string appointment.Location = event.location || "Location not defined!"; appointment.Body = new MessageBody(event.description || ""); // you can not use any special character or escape the content for (let i = 0; i < event.attendees.length; i++) { appointment.RequiredAttendees.Add(new Attendee(event.attendees[i].email)); } appointment.Update( ConflictResolutionMode.AlwaysOverwrite, SendInvitationsOrCancellationsMode.SendToAllAndSaveCopy ); } catch (reason) { this.log.error(reason); throw reason; } } async deleteEvent(uid: string) { try { const appointment = await Appointment.Bind( this.getExchangeService(), new ItemId(uid), new PropertySet() ); // Delete the appointment. Note that the item ID will change when the item is moved to the Deleted Items folder. appointment.Delete(DeleteMode.MoveToDeletedItems); } catch (reason) { this.log.error(reason); throw reason; } } async getAvailability(dateFrom: string, dateTo: string, selectedCalendars: IntegrationCalendar[]) { try { const externalCalendars = await this.listCalendars(); const calendarsToGetAppointmentsFrom = []; for (let i = 0; i < selectedCalendars.length; i++) { //Only select vaild calendars! (We get all all active calendars on the instance! even from different users!) for (let k = 0; k < externalCalendars.length; k++) { if (selectedCalendars[i].externalId == externalCalendars[k].externalId) { calendarsToGetAppointmentsFrom.push(selectedCalendars[i]); } } } const finaleRet = []; for (let i = 0; i < calendarsToGetAppointmentsFrom.length; i++) { const calendarFolderId = new FolderId(calendarsToGetAppointmentsFrom[i].externalId); const localReturn = await this.getExchangeService() .FindAppointments( calendarFolderId, new CalendarView(DateTime.Parse(dateFrom), DateTime.Parse(dateTo)) ) .then(function (params) { const ret: EventBusyDate[] = []; for (let k = 0; k < params.Items.length; k++) { if (params.Items[k].LegacyFreeBusyStatus != LegacyFreeBusyStatus.Free) { //Dont use this appointment if "ShowAs" was set to "free" ret.push({ start: new Date(params.Items[k].Start.ToISOString()), end: new Date(params.Items[k].End.ToISOString()), }); } } return ret; }); finaleRet.push(...localReturn); } return finaleRet; } catch (reason) { this.log.error(reason); throw reason; } } async listCalendars() { try { const allFolders: IntegrationCalendar[] = []; return this.getExchangeService() .FindFolders(WellKnownFolderName.MsgFolderRoot, new FolderView(1000)) .then(async (res) => { for (const k in res.Folders) { const f = res.Folders[k]; if (f.FolderClass == "IPF.Appointment") { //Select parent folder for all calendars allFolders.push({ externalId: f.Id.UniqueId, name: f.DisplayName ?? "", primary: true, //The first one is prime integration: this.integrationName, }); return await this.getExchangeService() .FindFolders(f.Id, new FolderView(1000)) .then((res) => { //Find all calendars inside calendar folder res.Folders.forEach((fs) => { allFolders.push({ externalId: fs.Id.UniqueId, name: fs.DisplayName ?? "", primary: false, integration: this.integrationName, }); }); return allFolders; }); } } return allFolders; }); } catch (reason) { this.log.error(reason); throw reason; } } private getExchangeService(): ExchangeService { const exch1 = new ExchangeService(this.exchangeVersion); exch1.Credentials = new WebCredentials(this.credentials.username, this.credentials.password); exch1.Url = new Uri(this.url); return exch1; } }