import { XhrApi } from "@ewsjs/xhr"; import type { FindFoldersResults, FindItemsResults } from "ews-javascript-api"; import { Appointment, Attendee, BasePropertySet, CalendarView, ConflictResolutionMode, DateTime, DeleteMode, ExchangeService, Folder, FolderId, FolderSchema, FolderTraversal, FolderView, ItemId, LegacyFreeBusyStatus, LogicalOperator, MessageBody, PropertySet, SearchFilter, SendInvitationsMode, SendInvitationsOrCancellationsMode, Uri, WebCredentials, WellKnownFolderName, } from "ews-javascript-api"; import { symmetricDecrypt } from "@calcom/lib/crypto"; import logger from "@calcom/lib/logger"; import type { Calendar, CalendarEvent, EventBusyDate, IntegrationCalendar, NewCalendarEventType, Person, } from "@calcom/types/Calendar"; import type { CredentialPayload } from "@calcom/types/Credential"; import { ExchangeAuthentication } from "../enums"; export default class ExchangeCalendarService implements Calendar { private integrationName = ""; private log: typeof logger; private payload; constructor(credential: CredentialPayload) { this.integrationName = "exchange_calendar"; this.log = logger.getChildLogger({ prefix: [`[[lib] ${this.integrationName}`] }); this.payload = JSON.parse( symmetricDecrypt(credential.key?.toString() || "", process.env.CALENDSO_ENCRYPTION_KEY || "") ); } async createEvent(event: CalendarEvent): Promise { const appointment: Appointment = new Appointment(this.getExchangeService()); appointment.Subject = event.title; appointment.Start = DateTime.Parse(event.startTime); appointment.End = DateTime.Parse(event.endTime); appointment.Location = event.location || ""; appointment.Body = new MessageBody(event.description || ""); event.attendees.forEach((attendee: Person) => { appointment.RequiredAttendees.Add(new Attendee(attendee.email)); }); if (event.team?.members) { event.team.members.forEach((member: Person) => { appointment.RequiredAttendees.Add(new Attendee(member.email)); }); } return appointment .Save(SendInvitationsMode.SendToAllAndSaveCopy) .then(() => { return { uid: appointment.Id.UniqueId, id: appointment.Id.UniqueId, password: "", type: "", url: "", additionalInfo: {}, }; }) .catch((reason) => { this.log.error(reason); throw reason; }); } async updateEvent( uid: string, event: CalendarEvent ): Promise { const appointment: Appointment = await Appointment.Bind(this.getExchangeService(), new ItemId(uid)); appointment.Subject = event.title; appointment.Start = DateTime.Parse(event.startTime); appointment.End = DateTime.Parse(event.endTime); appointment.Location = event.location || ""; appointment.Body = new MessageBody(event.description || ""); event.attendees.forEach((attendee: Person) => { appointment.RequiredAttendees.Add(new Attendee(attendee.email)); }); if (event.team?.members) { event.team.members.forEach((member) => { appointment.RequiredAttendees.Add(new Attendee(member.email)); }); } return appointment .Update( ConflictResolutionMode.AlwaysOverwrite, SendInvitationsOrCancellationsMode.SendToChangedAndSaveCopy ) .then(() => { return { uid: appointment.Id.UniqueId, id: appointment.Id.UniqueId, password: "", type: "", url: "", additionalInfo: {}, }; }) .catch((reason) => { this.log.error(reason); throw reason; }); } async deleteEvent(uid: string): Promise { const appointment: Appointment = await Appointment.Bind(this.getExchangeService(), new ItemId(uid)); return appointment.Delete(DeleteMode.MoveToDeletedItems).catch((reason) => { this.log.error(reason); throw reason; }); } async getAvailability( dateFrom: string, dateTo: string, selectedCalendars: IntegrationCalendar[] ): Promise { const calendars: IntegrationCalendar[] = await this.listCalendars(); const promises: Promise[] = calendars .filter((lcal) => selectedCalendars.some((rcal) => lcal.externalId == rcal.externalId)) .map(async (calendar) => { return this.getExchangeService() .FindAppointments( new FolderId(calendar.externalId), new CalendarView(DateTime.Parse(dateFrom), DateTime.Parse(dateTo)) ) .then((results: FindItemsResults) => { return results.Items.filter((appointment: Appointment) => { return appointment.LegacyFreeBusyStatus != LegacyFreeBusyStatus.Free; }).map((appointment: Appointment) => { return { start: new Date(appointment.Start.ToISOString()), end: new Date(appointment.End.ToISOString()), }; }); }) .catch((reason) => { this.log.error(reason); throw reason; }); }); return Promise.all(promises).then((x) => x.flat()); } async listCalendars(): Promise { const service: ExchangeService = this.getExchangeService(); const view: FolderView = new FolderView(1000); view.PropertySet = new PropertySet(BasePropertySet.IdOnly); view.PropertySet.Add(FolderSchema.ParentFolderId); view.PropertySet.Add(FolderSchema.DisplayName); view.PropertySet.Add(FolderSchema.ChildFolderCount); view.Traversal = FolderTraversal.Deep; const deletedItemsFolder: Folder = await Folder.Bind(service, WellKnownFolderName.DeletedItems); const searchFilterCollection = new SearchFilter.SearchFilterCollection(LogicalOperator.And); searchFilterCollection.Add(new SearchFilter.IsEqualTo(FolderSchema.FolderClass, "IPF.Appointment")); return service .FindFolders(WellKnownFolderName.MsgFolderRoot, searchFilterCollection, view) .then((res: FindFoldersResults) => { return res.Folders.filter((folder: Folder) => { return folder.ParentFolderId.UniqueId != deletedItemsFolder.Id.UniqueId; }).map((folder: Folder) => { return { externalId: folder.Id.UniqueId, name: folder.DisplayName ?? "", primary: folder.ChildFolderCount > 0, integration: this.integrationName, }; }); }) .catch((reason) => { this.log.error(reason); throw reason; }); } private getExchangeService(): ExchangeService { const service: ExchangeService = new ExchangeService(this.payload.exchangeVersion); service.Credentials = new WebCredentials(this.payload.username, this.payload.password); service.Url = new Uri(this.payload.url); if (this.payload.authenticationMethod === ExchangeAuthentication.NTLM) { const xhr: XhrApi = new XhrApi({ rejectUnauthorized: false, gzip: this.payload.useCompression, }).useNtlmAuthentication(this.payload.username, this.payload.password); service.XHRApi = xhr; } return service; } }