import type { IEmail, IEmailLogTraffic, TEmailLogTrafficWindow, } from '../../ts_interfaces/requests/email-ops.js'; const MINUTE_MS = 60_000; const HOUR_MS = 60 * MINUTE_MS; const DAY_MS = 24 * HOUR_MS; export const EMAIL_LOG_RETENTION_MS = 30 * DAY_MS; export const EMAIL_LOG_SEARCH_MAX_LENGTH = 200; export interface IEmailLogTrafficWindowDescriptor { window: TEmailLogTrafficWindow; label: string; durationMs: number; bucketSizeMs: number; } export interface IEmailLogSparseTrafficBucket { bucketStart: number; sent: number; received: number; failed: number; } const trafficWindows: IEmailLogTrafficWindowDescriptor[] = [ { window: '24h', label: 'Email Traffic — Last 24 Hours', durationMs: DAY_MS, bucketSizeMs: MINUTE_MS }, { window: '7d', label: 'Email Traffic — Last 7 Days', durationMs: 7 * DAY_MS, bucketSizeMs: 15 * MINUTE_MS }, { window: '14d', label: 'Email Traffic — Last 14 Days', durationMs: 14 * DAY_MS, bucketSizeMs: 30 * MINUTE_MS }, { window: '30d', label: 'Email Traffic — Last 30 Days', durationMs: 30 * DAY_MS, bucketSizeMs: HOUR_MS }, ]; export function normalizeEmailLogSearch(searchArg: unknown): string { if (searchArg === undefined || searchArg === null) return ''; if (typeof searchArg !== 'string') { throw new Error('Email log search must be a string'); } const search = searchArg.trim(); if (search.length > EMAIL_LOG_SEARCH_MAX_LENGTH) { throw new Error(`Email log search accepts at most ${EMAIL_LOG_SEARCH_MAX_LENGTH} characters`); } return search; } export function selectEmailLogTrafficWindow( searchArg: string, oldestMatchAtArg: number | null, nowArg = Date.now(), ): IEmailLogTrafficWindowDescriptor { if (!searchArg || oldestMatchAtArg === null || !Number.isFinite(oldestMatchAtArg)) { return trafficWindows[0]; } return trafficWindows.find((window) => { const lastBucketStart = Math.floor(nowArg / window.bucketSizeMs) * window.bucketSizeMs; const windowStart = lastBucketStart - window.durationMs + window.bucketSizeMs; return oldestMatchAtArg >= windowStart; }) || trafficWindows.at(-1)!; } export function buildEmailLogTraffic( descriptorArg: IEmailLogTrafficWindowDescriptor, sparseBucketsArg: IEmailLogSparseTrafficBucket[], nowArg = Date.now(), ): IEmailLogTraffic { const lastBucketStart = Math.floor(nowArg / descriptorArg.bucketSizeMs) * descriptorArg.bucketSizeMs; const windowStart = lastBucketStart - descriptorArg.durationMs + descriptorArg.bucketSizeMs; const bucketsByStart = new Map( sparseBucketsArg.map((bucket) => [bucket.bucketStart, bucket]), ); const sent: IEmailLogTraffic['sent'] = []; const received: IEmailLogTraffic['received'] = []; const failed: IEmailLogTraffic['failed'] = []; for ( let timestamp = windowStart; timestamp <= lastBucketStart; timestamp += descriptorArg.bucketSizeMs ) { const bucket = bucketsByStart.get(timestamp); sent.push({ timestamp, value: Math.max(0, Number(bucket?.sent || 0)) }); received.push({ timestamp, value: Math.max(0, Number(bucket?.received || 0)) }); failed.push({ timestamp, value: Math.max(0, Number(bucket?.failed || 0)) }); } return { ...descriptorArg, windowStart, windowEnd: nowArg, sent, received, failed, }; } export function aggregateEmailLogTrafficInMemory( emailsArg: IEmail[], descriptorArg: IEmailLogTrafficWindowDescriptor, nowArg = Date.now(), ): IEmailLogSparseTrafficBucket[] { const lastBucketStart = Math.floor(nowArg / descriptorArg.bucketSizeMs) * descriptorArg.bucketSizeMs; const windowStart = lastBucketStart - descriptorArg.durationMs + descriptorArg.bucketSizeMs; const buckets = new Map(); for (const email of emailsArg) { const timestamp = new Date(email.timestamp).getTime(); if (!Number.isFinite(timestamp) || timestamp < windowStart || timestamp > nowArg) continue; const bucketStart = Math.floor(timestamp / descriptorArg.bucketSizeMs) * descriptorArg.bucketSizeMs; const bucket = buckets.get(bucketStart) || { bucketStart, sent: 0, received: 0, failed: 0 }; if (email.status === 'bounced' || email.status === 'rejected') { bucket.failed++; } else if (email.direction === 'outbound') { bucket.sent++; } else { bucket.received++; } buckets.set(bucketStart, bucket); } return [...buckets.values()].sort((a, b) => a.bucketStart - b.bucketStart); }