feat: Mimecast multi-tenant held mail viewer

- migration 062: mimecast_tenants table (company_id, client_id/secret, account_code)
- Seed Wulf (CUSA13A95) + Seubert (CUSA96A181) tenants
- MimecastClient.getHeldMessages(): full pagination via meta.pagination.next cursor
  (API always returns 10/page regardless of pageSize param, totalCount in meta)
- getMimecastClientForTenant() factory for per-tenant instantiation
- GET /api/mimecast/held?tenantId=&recipient= — fetches all tenants in parallel,
  merges + sorts by date, returns per-tenant counts + combined messages[]
- Held Mail tab on /admin/sync/mimecast (on-demand load, recipient filter,
  tenant badges, policy filter dropdown, DMARC/impersonation highlighted red)
This commit is contained in:
lorentz 2026-03-31 22:38:22 -04:00
parent a98c0daf15
commit fcdec8e38b
12 changed files with 2208 additions and 27 deletions

View file

@ -73,6 +73,32 @@ export interface MimecastMessageInfo {
headers?: Record<string, string>;
}
export interface MimecastHeldMessage {
id: string;
subject: string;
from: string;
fromDisplay: string;
to: string;
toDisplay: string;
dateReceived: string;
reason: string;
reasonCode: string;
policyInfo: string;
route: string;
hasAttachments: boolean;
size: number;
}
export interface MimecastCloudUser {
emailAddress: string;
domain: string;
lockedOut: boolean;
status?: string;
name?: string;
alias?: string[];
[key: string]: any;
}
export interface PaginatedResult<T> {
items: T[];
nextCursor: string | null;
@ -398,6 +424,87 @@ export class MimecastClient {
}
}
// ── Cloud Gateway ─────────────────────────────────────────────────────────
/**
* GET /user/cloud-gateway/v1/users?emailAddress=...&domain=...
*/
async getCloudUser(emailAddress: string, domain: string): Promise<MimecastCloudUser | null> {
const data = await this.request<any>('GET', '/user/cloud-gateway/v1/users', undefined, {
emailAddress,
domain,
});
const user = data?.value?.[0] ?? data?.data?.[0] ?? null;
if (!user) return null;
return {
emailAddress: user.emailAddress ?? emailAddress,
domain: user.domain ?? domain,
lockedOut: user.lockedOut ?? false,
status: user.status ?? undefined,
name: user.name ?? undefined,
alias: user.alias ?? undefined,
_raw: user,
};
}
// ── Held Messages ──────────────────────────────────────────────────────────
/**
* POST /api/gateway/get-hold-message-list
* admin: true = see user-level hold queues as an admin
* pageSize is ignored by the API (always returns 10); paginate via meta.pagination.next
* Fetches ALL pages up to maxMessages limit.
*/
async getHeldMessages(options: {
recipient?: string;
maxMessages?: number;
} = {}): Promise<{ messages: MimecastHeldMessage[]; totalCount: number }> {
const maxMessages = options.maxMessages ?? 500;
const all: MimecastHeldMessage[] = [];
let cursor: string | null = null;
let totalCount = 0;
do {
const reqBody: any = { admin: true };
if (options.recipient) {
reqBody.searchBy = { fieldName: 'recipient', value: options.recipient };
}
const body: any = { data: [reqBody] };
if (cursor) {
body.meta = { pagination: { pageToken: cursor } };
}
const result = await this.request<any>('POST', '/api/gateway/get-hold-message-list', body);
const pagination = result?.meta?.pagination ?? {};
const msgs: any[] = result?.data ?? [];
if (totalCount === 0) totalCount = pagination.totalCount ?? msgs.length;
for (const m of msgs) {
all.push({
id: m.id,
subject: m.subject ?? '',
from: m.fromHeader?.emailAddress ?? m.from?.emailAddress ?? '',
fromDisplay: m.fromHeader?.displayableName ?? m.from?.displayableName ?? '',
to: m.to?.emailAddress ?? '',
toDisplay: m.to?.displayableName ?? '',
dateReceived: m.dateReceived ?? '',
reason: m.reason ?? '',
reasonCode: m.reasonCode ?? '',
policyInfo: m.policyInfo ?? '',
route: m.route ?? '',
hasAttachments: m.hasAttachments ?? false,
size: m.size ?? 0,
});
}
cursor = pagination.next ?? null;
} while (cursor && all.length < maxMessages);
return { messages: all, totalCount };
}
// ── Account ────────────────────────────────────────────────────────────────
/**
@ -436,3 +543,17 @@ export function getMimecastClient(): MimecastClient {
}
return _client;
}
export function getMimecastClientForTenant(tenant: {
client_id: string;
client_secret: string;
base_url?: string;
account_code?: string;
}): MimecastClient {
return new MimecastClient({
clientId: tenant.client_id,
clientSecret: tenant.client_secret,
baseUrl: tenant.base_url ?? 'https://api.services.mimecast.com',
accountCode: tenant.account_code ?? '',
});
}