128 lines
3.5 KiB
TypeScript
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 }),
|
|
});
|
|
}
|