/** * Mimecast API 2.0 Client * Auth: OAuth2 Client Credentials — POST /oauth/token * Base: https://api.services.mimecast.com * * Endpoints sourced from official Postman collection (mimecast-api-v2-collection.json): * - Message logs: POST /api/message-finder/search (trackedEmails with pagination) * - SIEM batch: GET /siem/v1/batch/events/cg (returns pre-signed URLs → download JSON) * - Threat events: GET /threats/v1/events * - Message info: POST /api/message-finder/get-message-info * - Account: POST /api/account/get-account */ export interface MimecastConfig { clientId: string; clientSecret: string; baseUrl?: string; } interface TokenResponse { access_token: string; token_type: string; expires_in: number; } interface TokenCache { token: string; expiresAt: number; } export interface MimecastMessage { id: string; senderAddress: string; recipientAddress: string; subject: string; direction: string; status: string; action?: string; spamScore?: number; sizeBytes?: number; attachmentCount?: boolean | number; sentDateTime?: string; receivedDateTime?: string; route?: string; rejectReason?: string; heldReason?: string; sourceIp?: string; [key: string]: any; } export interface MimecastThreatEvent { id: string; messageId?: string; eventType: string; threatLevel?: string; url?: string; fileName?: string; verdict?: string; actorEmail?: string; eventDateTime?: string; analysis?: string[]; source?: string[]; direction?: string[]; status?: string[]; [key: string]: any; } export interface MimecastMessageInfo { messageId: string; bodyText?: string; bodyHtml?: string; headers?: Record; } export interface PaginatedResult { items: T[]; nextCursor: string | null; totalCount?: number; } export class MimecastClient { private readonly clientId: string; private readonly clientSecret: string; private readonly baseUrl: string; private tokenCache: TokenCache | null = null; constructor(config: MimecastConfig) { this.clientId = config.clientId; this.clientSecret = config.clientSecret; this.baseUrl = (config.baseUrl ?? 'https://api.services.mimecast.com').replace(/\/$/, ''); } // ── Auth ───────────────────────────────────────────────────────────────── private async getToken(): Promise { if (this.tokenCache && Date.now() < this.tokenCache.expiresAt - 60_000) { return this.tokenCache.token; } const body = new URLSearchParams({ grant_type: 'client_credentials', client_id: this.clientId, client_secret: this.clientSecret, }); const res = await fetch(`${this.baseUrl}/oauth/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: body.toString(), }); if (!res.ok) { const text = await res.text(); throw new Error(`Mimecast auth failed (${res.status}): ${text}`); } const data: TokenResponse = await res.json(); this.tokenCache = { token: data.access_token, expiresAt: Date.now() + data.expires_in * 1000, }; return data.access_token; } private async request( method: 'GET' | 'POST', path: string, body?: Record, params?: Record ): Promise { const token = await this.getToken(); let url = `${this.baseUrl}${path}`; if (params && Object.keys(params).length > 0) { url = `${url}?${new URLSearchParams(params).toString()}`; } const res = await fetch(url, { method, headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', Accept: 'application/json', }, body: body ? JSON.stringify(body) : undefined, }); if (!res.ok) { const text = await res.text(); throw new Error(`Mimecast ${method} ${path} failed (${res.status}): ${text}`); } return res.json() as T; } // ── Message Tracking ───────────────────────────────────────────────────── /** * Search tracked emails via POST /api/message-finder/search * Response: { data: [{ trackedEmails: [...], pageToken: string }] } */ async getMessageLogs(options: { from: string; // ISO datetime string to: string; cursor?: string; pageSize?: number; }): Promise> { const reqData: Record = { from: options.from, to: options.to, pageSize: options.pageSize ?? 500, }; if (options.cursor) reqData.pageToken = options.cursor; const data = await this.request('POST', '/api/message-finder/search', { data: [reqData] }); const block = data.data?.[0] ?? {}; const tracked: any[] = block.trackedEmails ?? []; return { items: tracked.map((m: any) => this.normalizeTrackedEmail(m)), nextCursor: block.pageToken ?? null, totalCount: tracked.length, }; } private normalizeTrackedEmail(m: any): MimecastMessage { const sender = m.fromEnv?.emailAddress ?? m.fromHdr?.emailAddress ?? m.sender ?? ''; const recipients: string[] = (m.to ?? []).map((t: any) => typeof t === 'string' ? t : t.emailAddress ?? '' ); return { id: m.id, senderAddress: sender, recipientAddress: recipients.join(', '), subject: m.subject ?? '', direction: m.route?.toLowerCase().includes('inbound') ? 'inbound' : 'outbound', status: m.status ?? '', action: m.detectionLevel ?? undefined, spamScore: m.spamScore ?? undefined, sizeBytes: undefined, attachmentCount: m.attachments ?? 0, sentDateTime: m.sent ?? undefined, receivedDateTime: m.received ?? undefined, route: m.route ?? undefined, rejectReason: m.info ?? undefined, sourceIp: m.senderIP ?? undefined, _raw: m, }; } // ── SIEM Batch Events ───────────────────────────────────────────────────── /** * GET /siem/v1/batch/events/cg — returns pre-signed download URLs for batch NDJSON files * Each URL points to a compressed file; we download and parse each one. * Response: { value: [{url, expiry, size}], "@nextPage": string } */ async getSiemBatchEventUrls(options: { from: string; // date only: YYYY-MM-DD to: string; type?: string; cursor?: string; pageSize?: number; }): Promise<{ urls: Array<{ url: string; expiry: string; size: number }>; nextCursor: string | null }> { const params: Record = { type: options.type ?? 'mailflow', dateRangeStartsAt: options.from, dateRangeEndsAt: options.to, pageSize: String(options.pageSize ?? 500), }; if (options.cursor) params.pageToken = options.cursor; const data = await this.request('GET', '/siem/v1/batch/events/cg', undefined, params); return { urls: data.value ?? [], nextCursor: data['@nextPage'] ?? null, }; } /** * Download and parse a SIEM batch event file (NDJSON, possibly gzipped) */ async downloadSiemBatchFile(url: string): Promise { const res = await fetch(url); if (!res.ok) return []; const text = await res.text(); const lines = text.split('\n').filter(l => l.trim()); const messages: MimecastMessage[] = []; for (const line of lines) { try { const event = JSON.parse(line); messages.push(this.normalizeSiemEvent(event)); } catch { // skip malformed lines } } return messages; } private normalizeSiemEvent(e: any): MimecastMessage { return { id: e.messageId ?? e.id ?? e.Messageid ?? '', senderAddress: e.senderAddress ?? e.Sender ?? e.sender ?? '', recipientAddress: e.recipientAddress ?? e.Recipient ?? e.recipient ?? '', subject: e.subject ?? e.Subject ?? '', direction: (e.Dir ?? e.direction ?? '').toLowerCase() === 'inbound' ? 'inbound' : 'outbound', status: e.Act ?? e.action ?? e.status ?? '', action: e.RejType ?? e.rejectionType ?? undefined, spamScore: e.SpamScore ?? e.spamScore ?? undefined, sizeBytes: e.MsgSize ?? e.messageSize ?? e.size ?? undefined, attachmentCount: e.AttCnt ?? e.attachmentCount ?? 0, sentDateTime: e.Datetime ?? e.datetime ?? e.timestamp ?? undefined, receivedDateTime: e.Datetime ?? undefined, route: e.Route ?? e.route ?? undefined, rejectReason: e.RejCode ?? e.rejectionCode ?? undefined, heldReason: e.HeldGroup ?? undefined, sourceIp: e.IP ?? e.senderIp ?? undefined, _raw: e, }; } // ── Threat Events ───────────────────────────────────────────────────────── /** * GET /threats/v1/events * Response: { value: [{timestamp, analysis, source, details, status, direction, subject, sender, ...}], "@nextPage": string } */ async getThreatEvents(options: { cursor?: string; pageSize?: number; } = {}): Promise> { const params: Record = { pageSize: String(options.pageSize ?? 500), }; if (options.cursor) params.pageToken = options.cursor; try { const data = await this.request('GET', '/threats/v1/events', undefined, params); const items = (data.value ?? []).map((e: any) => this.normalizeThreatEvent(e)); return { items, nextCursor: data['@nextPage'] ?? null, }; } catch { return { items: [], nextCursor: null }; } } private normalizeThreatEvent(e: any): MimecastThreatEvent { const analysis: string[] = Array.isArray(e.analysis) ? e.analysis : [e.analysis].filter(Boolean); const eventType = analysis[0] ?? 'unknown'; const statusArr: string[] = Array.isArray(e.status) ? e.status : [e.status].filter(Boolean); const verdict = statusArr[0] ?? undefined; const threatLevel = analysis.includes('malware') ? 'high' : analysis.includes('phishing') ? 'high' : analysis.includes('spam') ? 'medium' : 'info'; return { id: e.id ?? `threat_${e.sender ?? ''}_${e.timestamp ?? Date.now()}`, messageId: e.messageId ?? undefined, eventType, threatLevel, url: e.url ?? undefined, fileName: undefined, verdict, actorEmail: e.sender ?? undefined, eventDateTime: e.timestamp ?? undefined, analysis, source: Array.isArray(e.source) ? e.source : [e.source].filter(Boolean), direction: Array.isArray(e.direction) ? e.direction : [e.direction].filter(Boolean), status: statusArr, _raw: e, }; } // ── Message Info (body/headers) ──────────────────────────────────────────── /** * POST /api/message-finder/get-message-info * Returns delivered message details including parts */ async getMessageInfo(messageId: string): Promise { try { const data = await this.request('POST', '/api/message-finder/get-message-info', { data: [{ id: messageId }], }); const delivered = data.data?.[0]?.deliveredMessage; if (!delivered) return null; const entries = Object.values(delivered) as any[]; if (!entries.length) return null; const info = entries[0]?.messageInfo ?? entries[0]; return { messageId, bodyText: info?.textBody ?? info?.body ?? undefined, bodyHtml: info?.htmlBody ?? undefined, headers: info?.headers ?? undefined, }; } catch { return null; } } // ── Account ──────────────────────────────────────────────────────────────── /** * POST /api/account/get-account */ async testConnection(): Promise<{ ok: boolean; accountName?: string; packageName?: string; error?: string }> { try { const data = await this.request('POST', '/api/account/get-account', { data: [{}] }); const account = data.data?.[0] ?? {}; return { ok: true, accountName: account.accountName ?? account.accountCode ?? 'Connected', packageName: account.packageName ?? undefined, }; } catch (err: any) { return { ok: false, error: err.message }; } } } let _client: MimecastClient | null = null; export function getMimecastClient(): MimecastClient { if (!_client) { const clientId = process.env.MIMECAST_CLIENT_ID; const clientSecret = process.env.MIMECAST_CLIENT_SECRET; if (!clientId || !clientSecret) { throw new Error('MIMECAST_CLIENT_ID and MIMECAST_CLIENT_SECRET must be set'); } _client = new MimecastClient({ clientId, clientSecret, baseUrl: process.env.MIMECAST_BASE_URL ?? 'https://api.services.mimecast.com', }); } return _client; }