feat: mailbox remediation via Graph Mail.ReadWrite — search + move to Deleted Items from analysis dialog

- Add searchMailboxMessages, deleteMailboxMessage, moveToDeletedItems to MsGraphClient
- POST /api/mimecast/mailbox-remediate: search, move, delete actions with permission error handling
- DeliveredAnalysisDialog: Remove from mailbox panel with search → confirm → delete flow
  - Shows matching messages in mailbox with checkboxes, received time, read/unread status
  - Moves selected to Deleted Items (recoverable) via Graph API
  - Surfaces clear permission guidance if Mail.ReadWrite not yet granted
This commit is contained in:
lorentz 2026-04-01 10:09:38 -04:00
parent c2ebbe586b
commit bc3904de4e
3 changed files with 316 additions and 1 deletions

View file

@ -330,6 +330,82 @@ export class MsGraphClient {
return messages;
}
/**
* POST /users/{user}/messages search by sender and/or subject in a date range.
* Requires Mail.ReadWrite application permission.
*/
async searchMailboxMessages(options: {
userEmail: string;
fromAddress?: string;
subject?: string;
receivedAfter?: string;
receivedBefore?: string;
maxResults?: number;
}): Promise<{ id: string; subject: string; from: string; receivedDateTime: string; isRead: boolean }[]> {
const filters: string[] = [];
if (options.fromAddress) {
filters.push(`from/emailAddress/address eq '${options.fromAddress.replace(/'/g, "''")}'`);
}
if (options.subject) {
filters.push(`contains(subject,'${options.subject.replace(/'/g, "''")}') `);
}
if (options.receivedAfter) {
filters.push(`receivedDateTime ge ${options.receivedAfter}`);
}
if (options.receivedBefore) {
filters.push(`receivedDateTime le ${options.receivedBefore}`);
}
if (!filters.length) throw new Error('At least one search filter required');
const top = Math.min(options.maxResults ?? 50, 100);
const qs = `$filter=${encodeURIComponent(filters.join(' and '))}&$select=id,subject,from,receivedDateTime,isRead&$top=${top}&$orderby=receivedDateTime desc`;
const url = `/users/${encodeURIComponent(options.userEmail)}/messages?${qs}`;
const data = await this.fetchJson<{ value: any[] }>(url);
return (data.value ?? []).map(m => ({
id: m.id,
subject: m.subject ?? '',
from: m.from?.emailAddress?.address ?? '',
receivedDateTime: m.receivedDateTime ?? '',
isRead: m.isRead ?? false,
}));
}
/**
* DELETE /users/{user}/messages/{messageId}
* Permanently deletes a message. Requires Mail.ReadWrite application permission.
*/
async deleteMailboxMessage(userEmail: string, messageId: string): Promise<void> {
const token = await this.getToken();
const url = `https://graph.microsoft.com/v1.0/users/${encodeURIComponent(userEmail)}/messages/${encodeURIComponent(messageId)}`;
const res = await fetch(url, {
method: 'DELETE',
headers: { Authorization: `Bearer ${token}` },
});
if (res.status === 204) return;
if (res.status === 404) return; // already gone
const text = await res.text();
throw new Error(`Graph delete failed ${res.status}: ${text}`);
}
/**
* Move a message to Deleted Items (soft delete recoverable).
* Requires Mail.ReadWrite application permission.
*/
async moveToDeletedItems(userEmail: string, messageId: string): Promise<void> {
const token = await this.getToken();
const url = `https://graph.microsoft.com/v1.0/users/${encodeURIComponent(userEmail)}/messages/${encodeURIComponent(messageId)}/move`;
const res = await fetch(url, {
method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ destinationId: 'deleteditems' }),
});
if (res.ok) return;
if (res.status === 404) return;
const text = await res.text();
throw new Error(`Graph move failed ${res.status}: ${text}`);
}
/**
* Get calendar events for a user in a date range (paginated).
* Returns empty array and logs if the mailbox is not Exchange Online (graceful degradation).