Files
OL-Rescue/src/graph.ts

128 lines
3.5 KiB
TypeScript

const GRAPH = "https://graph.microsoft.com/v1.0";
function escapeODataString(s: string): string {
return s.replace(/'/g, "''");
}
export interface GraphMessage {
id: string;
subject?: string;
}
export interface GraphMailFolder {
id: string;
displayName: string;
}
async function graphJson<T>(token: string, pathOrUrl: string, init?: RequestInit): Promise<T> {
const url = pathOrUrl.startsWith("https://") ? pathOrUrl : `${GRAPH}${pathOrUrl}`;
const res = await fetch(url, {
...init,
headers: {
Authorization: `Bearer ${token}`,
...(init?.headers ?? {}),
},
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Graph ${res.status}: ${text || res.statusText}`);
}
return res.json() as Promise<T>;
}
export async function listInboxChildFolders(token: string): Promise<GraphMailFolder[]> {
const out: GraphMailFolder[] = [];
let next: string | null =
"/me/mailFolders/inbox/childFolders?$select=id,displayName&$top=999";
while (next) {
const page = await graphJson<{ value: GraphMailFolder[]; "@odata.nextLink"?: string }>(
token,
next,
);
out.push(...page.value);
next = page["@odata.nextLink"] ?? null;
}
return out;
}
export async function findChildFolderByName(
token: string,
displayName: string,
): Promise<GraphMailFolder | undefined> {
const folders = await listInboxChildFolders(token);
return folders.find((f) => f.displayName === displayName);
}
export async function createInboxChildFolder(
token: string,
displayName: string,
): Promise<GraphMailFolder> {
return graphJson<GraphMailFolder>(token, "/me/mailFolders/inbox/childFolders", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ displayName }),
});
}
export async function findOrCreateInboxChildFolder(
token: string,
displayName: string,
): Promise<GraphMailFolder> {
const existing = await findChildFolderByName(token, displayName);
if (existing) {
return existing;
}
return createInboxChildFolder(token, displayName);
}
async function listMessagesMatchingFilter(
token: string,
filter: string,
): Promise<GraphMessage[]> {
const out: GraphMessage[] = [];
let next: string | null =
`/me/messages?$filter=${encodeURIComponent(filter)}&$select=id,subject&$top=999`;
while (next) {
const page = await graphJson<{ value: GraphMessage[]; "@odata.nextLink"?: string }>(
token,
next,
);
out.push(...page.value);
next = page["@odata.nextLink"] ?? null;
}
return out;
}
/**
* Finds messages whose subject or preview contains "TrackingID#<id>".
* Falls back to subject-only filter if the tenant rejects bodyPreview in $filter.
*/
export async function findMessagesWithTrackingId(
token: string,
trackingId: string,
): Promise<GraphMessage[]> {
const needle = `TrackingID#${trackingId}`;
const q = escapeODataString(needle);
const combined = `(contains(subject,'${q}') or contains(bodyPreview,'${q}'))`;
const subjectOnly = `contains(subject,'${q}')`;
try {
return await listMessagesMatchingFilter(token, combined);
} catch {
return listMessagesMatchingFilter(token, subjectOnly);
}
}
export async function moveMessageToFolder(
token: string,
messageId: string,
destinationFolderId: string,
): Promise<void> {
await graphJson<unknown>(token, `/me/messages/${encodeURIComponent(messageId)}/move`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ destinationId: destinationFolderId }),
});
}