205 lines
6.8 KiB
TypeScript
205 lines
6.8 KiB
TypeScript
|
import { XhrApi } from "@ewsjs/xhr";
|
||
|
import { Credential } from "@prisma/client";
|
||
|
import {
|
||
|
Appointment,
|
||
|
Attendee,
|
||
|
BasePropertySet,
|
||
|
CalendarView,
|
||
|
ConflictResolutionMode,
|
||
|
DateTime,
|
||
|
DeleteMode,
|
||
|
ExchangeService,
|
||
|
FindFoldersResults,
|
||
|
FindItemsResults,
|
||
|
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 {
|
||
|
Calendar,
|
||
|
CalendarEvent,
|
||
|
EventBusyDate,
|
||
|
IntegrationCalendar,
|
||
|
NewCalendarEventType,
|
||
|
Person,
|
||
|
} from "@calcom/types/Calendar";
|
||
|
|
||
|
import { ExchangeAuthentication } from "../enums";
|
||
|
|
||
|
export default class ExchangeCalendarService implements Calendar {
|
||
|
private integrationName = "";
|
||
|
private log: typeof logger;
|
||
|
private payload;
|
||
|
|
||
|
constructor(credential: Credential) {
|
||
|
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<NewCalendarEventType> {
|
||
|
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));
|
||
|
});
|
||
|
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<NewCalendarEventType | NewCalendarEventType[]> {
|
||
|
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));
|
||
|
});
|
||
|
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<void> {
|
||
|
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<EventBusyDate[]> {
|
||
|
const calendars: IntegrationCalendar[] = await this.listCalendars();
|
||
|
const promises: Promise<EventBusyDate[]>[] = 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<Appointment>) => {
|
||
|
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<IntegrationCalendar[]> {
|
||
|
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();
|
||
|
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;
|
||
|
}
|
||
|
}
|