feat: more tools
This commit was merged in pull request #2.
This commit is contained in:
+204
-1
@@ -250,6 +250,13 @@ export class EwsClient {
|
||||
traversal = "Shallow",
|
||||
} = options;
|
||||
|
||||
const isDistinguished =
|
||||
DISTINGUISHED_FOLDERS[folderId.toLowerCase()] !== undefined;
|
||||
|
||||
const folderIdXml = isDistinguished
|
||||
? `<t:DistinguishedFolderId Id="${folderId}"/>`
|
||||
: `<t:FolderId Id="${folderId}"/>`;
|
||||
|
||||
const soap = buildSoapEnvelope(`\
|
||||
<m:FindItem Traversal="${traversal}">
|
||||
<m:ItemShape>
|
||||
@@ -257,7 +264,7 @@ export class EwsClient {
|
||||
</m:ItemShape>
|
||||
<m:IndexedPageItemView MaxEntriesReturned="${limit}" Offset="${offset}" BasePoint="Beginning"/>
|
||||
<m:ParentFolderIds>
|
||||
<t:FolderId Id="${folderId}"/>
|
||||
${folderIdXml}
|
||||
</m:ParentFolderIds>
|
||||
<m:SortOrder>
|
||||
<t:FieldOrder Order="Descending">
|
||||
@@ -523,4 +530,200 @@ ${restriction}
|
||||
text = text.replace(/[ \t]+/g, " ");
|
||||
return text.trim();
|
||||
}
|
||||
|
||||
// ── UpdateItem (for marking read/unread, etc.) ──────────────────────────
|
||||
|
||||
async updateItems(
|
||||
itemIds: string[],
|
||||
updates: { fieldUri: string; value: string | boolean }[],
|
||||
): Promise<any[]> {
|
||||
const propName = (fieldUri: string): string => {
|
||||
const parts = fieldUri.split(":");
|
||||
return parts[parts.length - 1];
|
||||
};
|
||||
|
||||
const changesXml = itemIds.map((id) => {
|
||||
const updatesXml = updates.map((u) => {
|
||||
const val = typeof u.value === "boolean"
|
||||
? (u.value ? "true" : "false")
|
||||
: u.value;
|
||||
return `\
|
||||
<t:SetItemField>
|
||||
<t:FieldURI FieldURI="${u.fieldUri}"/>
|
||||
<t:Message>
|
||||
<t:${propName(u.fieldUri)}>${String(val)}</t:${
|
||||
propName(u.fieldUri)
|
||||
}>
|
||||
</t:Message>
|
||||
</t:SetItemField>`;
|
||||
}).join("\n");
|
||||
|
||||
return `\
|
||||
<t:ItemChange>
|
||||
<t:ItemId Id="${id}"/>
|
||||
<t:Updates>
|
||||
${updatesXml}
|
||||
</t:Updates>
|
||||
</t:ItemChange>`;
|
||||
}).join("\n");
|
||||
|
||||
const soap = buildSoapEnvelope(`\
|
||||
<m:UpdateItem MessageDisposition="SaveOnly" ConflictResolution="AutoResolve">
|
||||
<m:ItemChanges>
|
||||
${changesXml}
|
||||
</m:ItemChanges>
|
||||
</m:UpdateItem>`);
|
||||
|
||||
const data = await this.soapRequest(
|
||||
soap,
|
||||
"http://schemas.microsoft.com/exchange/services/2006/messages/UpdateItem",
|
||||
);
|
||||
|
||||
return this.extractResponseMessages(data);
|
||||
}
|
||||
|
||||
// ── GetAttachment ───────────────────────────────────────────────────────
|
||||
|
||||
async getAttachment(
|
||||
attachmentId: string,
|
||||
): Promise<{ content: Buffer; name: string; contentType: string }> {
|
||||
const soap = buildSoapEnvelope(`\
|
||||
<m:GetAttachment>
|
||||
<m:AttachmentIds>
|
||||
<t:AttachmentId Id="${attachmentId}"/>
|
||||
</m:AttachmentIds>
|
||||
</m:GetAttachment>`);
|
||||
|
||||
const data = await this.soapRequest(
|
||||
soap,
|
||||
"http://schemas.microsoft.com/exchange/services/2006/messages/GetAttachment",
|
||||
);
|
||||
|
||||
for (const msg of this.extractResponseMessages(data)) {
|
||||
const attachments = msg?.Attachments?.FileAttachment;
|
||||
if (!attachments) continue;
|
||||
const list = Array.isArray(attachments) ? attachments : [attachments];
|
||||
for (const a of list) {
|
||||
const contentBase64 = a.Content ?? "";
|
||||
const content = Buffer.from(contentBase64, "base64");
|
||||
return {
|
||||
content,
|
||||
name: a.Name ?? "attachment",
|
||||
contentType: a.ContentType ?? "application/octet-stream",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Attachment '${attachmentId}' not found`);
|
||||
}
|
||||
|
||||
// ── FindItem with CalendarView ──────────────────────────────────────────
|
||||
|
||||
async findCalendarItems(
|
||||
folderId: string,
|
||||
startDate: string,
|
||||
endDate: string,
|
||||
): Promise<any[]> {
|
||||
const soap = buildSoapEnvelope(`\
|
||||
<m:FindItem Traversal="Shallow">
|
||||
<m:ItemShape>
|
||||
<t:BaseShape>AllProperties</t:BaseShape>
|
||||
<t:BodyType>HTML</t:BodyType>
|
||||
</m:ItemShape>
|
||||
<m:ParentFolderIds>
|
||||
<t:FolderId Id="${folderId}"/>
|
||||
</m:ParentFolderIds>
|
||||
<m:CalendarView MaxEntriesReturned="200" StartDate="${startDate}" EndDate="${endDate}"/>
|
||||
</m:FindItem>`);
|
||||
|
||||
const data = await this.soapRequest(
|
||||
soap,
|
||||
"http://schemas.microsoft.com/exchange/services/2006/messages/FindItem",
|
||||
);
|
||||
|
||||
for (const msg of this.extractResponseMessages(data)) {
|
||||
const items = msg?.RootFolder?.Items?.CalendarItem;
|
||||
if (!items) continue;
|
||||
return Array.isArray(items) ? items : [items];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
// ── GetUserAvailability ─────────────────────────────────────────────────
|
||||
|
||||
async getUserAvailability(
|
||||
emails: string[],
|
||||
startDate: string,
|
||||
endDate: string,
|
||||
requestedView: string = "DetailedMerged",
|
||||
): Promise<any> {
|
||||
const mailboxDataXml = emails.map((email) =>
|
||||
`\
|
||||
<t:MailboxData>
|
||||
<t:Email>
|
||||
<t:Address>${
|
||||
email.replace(/&/g, "&").replace(/</g, "<")
|
||||
}</t:Address>
|
||||
</t:Email>
|
||||
<t:AttendeeType>Required</t:AttendeeType>
|
||||
</t:MailboxData>`
|
||||
).join("\n");
|
||||
|
||||
const soap = `<?xml version="1.0" encoding="utf-8"?>
|
||||
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
|
||||
xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages"
|
||||
xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types">
|
||||
<s:Body>
|
||||
<m:GetUserAvailabilityRequest>
|
||||
<m:MailboxDataArray>
|
||||
${mailboxDataXml}
|
||||
</m:MailboxDataArray>
|
||||
<m:FreeBusyViewOptions>
|
||||
<t:TimeWindow>
|
||||
<t:StartTime>${startDate}T00:00:00</t:StartTime>
|
||||
<t:EndTime>${endDate}T23:59:59</t:EndTime>
|
||||
</t:TimeWindow>
|
||||
<t:MergedFreeBusyIntervalInMinutes>30</t:MergedFreeBusyIntervalInMinutes>
|
||||
<t:RequestedView>${requestedView}</t:RequestedView>
|
||||
</m:FreeBusyViewOptions>
|
||||
</m:GetUserAvailabilityRequest>
|
||||
</s:Body>
|
||||
</s:Envelope>`;
|
||||
|
||||
const data = await this.soapRequest(
|
||||
soap,
|
||||
"http://schemas.microsoft.com/exchange/services/2006/messages/GetUserAvailability",
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// ── ResolveNames (directory search) ─────────────────────────────────────
|
||||
|
||||
async resolveNames(
|
||||
query: string,
|
||||
fullContact: boolean = true,
|
||||
): Promise<any[]> {
|
||||
const soap = buildSoapEnvelope(`\
|
||||
<m:ResolveNames ReturnFullContactData="${fullContact}" SearchScope="ActiveDirectoryContacts">
|
||||
<m:UnresolvedEntry>${
|
||||
query.replace(/&/g, "&").replace(/</g, "<")
|
||||
}</m:UnresolvedEntry>
|
||||
</m:ResolveNames>`);
|
||||
|
||||
const data = await this.soapRequest(
|
||||
soap,
|
||||
"http://schemas.microsoft.com/exchange/services/2006/messages/ResolveNames",
|
||||
);
|
||||
|
||||
for (const msg of this.extractResponseMessages(data)) {
|
||||
const resolutions = msg?.ResolutionSet?.Resolution;
|
||||
if (resolutions) {
|
||||
return Array.isArray(resolutions) ? resolutions : [resolutions];
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user