Files
dcrouter/ts/opsserver/handlers/email-ops.handler.ts

326 lines
10 KiB
TypeScript
Raw Normal View History

import * as plugins from '../../plugins.js';
import type { OpsServer } from '../classes.opsserver.js';
import * as interfaces from '../../../ts_interfaces/index.js';
import { SecurityLogger } from '../../security/index.js';
export class EmailOpsHandler {
public typedrouter = new plugins.typedrequest.TypedRouter();
constructor(private opsServerRef: OpsServer) {
// Add this handler's router to the parent
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
this.registerHandlers();
}
private registerHandlers(): void {
// Get Queued Emails Handler
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetQueuedEmails>(
'getQueuedEmails',
async (dataArg) => {
const emailServer = this.opsServerRef.dcRouterRef.emailServer;
if (!emailServer?.deliveryQueue) {
return { items: [], total: 0 };
}
const queue = emailServer.deliveryQueue;
const stats = queue.getStats();
// Get all queue items and filter by status if provided
const items = this.getQueueItems(
dataArg.status,
dataArg.limit || 50,
dataArg.offset || 0
);
return {
items,
total: stats.queueSize,
};
}
)
);
// Get Sent Emails Handler
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetSentEmails>(
'getSentEmails',
async (dataArg) => {
const items = this.getQueueItems(
'delivered',
dataArg.limit || 50,
dataArg.offset || 0
);
return {
items,
total: items.length, // Note: total would ideally come from a counter
};
}
)
);
// Get Failed Emails Handler
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetFailedEmails>(
'getFailedEmails',
async (dataArg) => {
const items = this.getQueueItems(
'failed',
dataArg.limit || 50,
dataArg.offset || 0
);
return {
items,
total: items.length,
};
}
)
);
// Resend Failed Email Handler
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ResendEmail>(
'resendEmail',
async (dataArg) => {
const emailServer = this.opsServerRef.dcRouterRef.emailServer;
if (!emailServer?.deliveryQueue) {
return { success: false, error: 'Email server not available' };
}
const queue = emailServer.deliveryQueue;
const item = queue.getItem(dataArg.emailId);
if (!item) {
return { success: false, error: 'Email not found in queue' };
}
if (item.status !== 'failed') {
return { success: false, error: `Email is not in failed state (current: ${item.status})` };
}
try {
// Re-enqueue the failed email by creating a new queue entry
// with the same data but reset attempt count
const newQueueId = await queue.enqueue(
item.processingResult,
item.processingMode,
item.route
);
// Optionally remove the old failed entry
await queue.removeItem(dataArg.emailId);
return { success: true, newQueueId };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to resend email'
};
}
}
)
);
// Get Security Incidents Handler
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetSecurityIncidents>(
'getSecurityIncidents',
async (dataArg) => {
const securityLogger = SecurityLogger.getInstance();
const filter: {
level?: any;
type?: any;
} = {};
if (dataArg.level) {
filter.level = dataArg.level;
}
if (dataArg.type) {
filter.type = dataArg.type;
}
const incidents = securityLogger.getRecentEvents(
dataArg.limit || 100,
Object.keys(filter).length > 0 ? filter : undefined
);
return {
incidents: incidents.map(event => ({
timestamp: event.timestamp,
level: event.level as interfaces.requests.TSecurityLogLevel,
type: event.type as interfaces.requests.TSecurityEventType,
message: event.message,
details: event.details,
ipAddress: event.ipAddress,
userId: event.userId,
sessionId: event.sessionId,
emailId: event.emailId,
domain: event.domain,
action: event.action,
result: event.result,
success: event.success,
})),
total: incidents.length,
};
}
)
);
// Get Bounce Records Handler
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetBounceRecords>(
'getBounceRecords',
async (dataArg) => {
const emailServer = this.opsServerRef.dcRouterRef.emailServer;
// Get bounce manager from email server via reflection
// BounceManager is private but we need to access it
const bounceManager = (emailServer as any)?.bounceManager;
if (!bounceManager) {
return { records: [], suppressionList: [], total: 0 };
}
// Get suppression list
const suppressionList = bounceManager.getSuppressionList();
// Get hard bounced addresses and convert to records
const hardBouncedAddresses = bounceManager.getHardBouncedAddresses();
// Create bounce records from the available data
const records: interfaces.requests.IBounceRecord[] = [];
for (const email of hardBouncedAddresses) {
const bounceInfo = bounceManager.getBounceInfo(email);
if (bounceInfo) {
records.push({
id: `bounce-${email}`,
recipient: email,
sender: '',
domain: email.split('@')[1] || '',
bounceType: bounceInfo.type as interfaces.requests.TBounceType,
bounceCategory: bounceInfo.category as interfaces.requests.TBounceCategory,
timestamp: bounceInfo.lastBounce,
processed: true,
});
}
}
// Apply limit and offset
const limit = dataArg.limit || 50;
const offset = dataArg.offset || 0;
const paginatedRecords = records.slice(offset, offset + limit);
return {
records: paginatedRecords,
suppressionList,
total: records.length,
};
}
)
);
// Remove from Suppression List Handler
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RemoveFromSuppressionList>(
'removeFromSuppressionList',
async (dataArg) => {
const emailServer = this.opsServerRef.dcRouterRef.emailServer;
const bounceManager = (emailServer as any)?.bounceManager;
if (!bounceManager) {
return { success: false, error: 'Bounce manager not available' };
}
try {
bounceManager.removeFromSuppressionList(dataArg.email);
return { success: true };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to remove from suppression list'
};
}
}
)
);
}
/**
* Helper method to get queue items with filtering and pagination
*/
private getQueueItems(
status?: interfaces.requests.TEmailQueueStatus,
limit: number = 50,
offset: number = 0
): interfaces.requests.IEmailQueueItem[] {
const emailServer = this.opsServerRef.dcRouterRef.emailServer;
if (!emailServer?.deliveryQueue) {
return [];
}
const queue = emailServer.deliveryQueue;
const items: interfaces.requests.IEmailQueueItem[] = [];
// Access the internal queue map via reflection
// This is necessary because the queue doesn't expose iteration methods
const queueMap = (queue as any).queue as Map<string, any>;
if (!queueMap) {
return [];
}
// Filter and convert items
for (const [id, item] of queueMap.entries()) {
// Apply status filter if provided
if (status && item.status !== status) {
continue;
}
// Extract email details from processingResult if available
const processingResult = item.processingResult;
let from = '';
let to: string[] = [];
let subject = '';
if (processingResult) {
// Check if it's an Email object or raw email data
if (processingResult.email) {
from = processingResult.email.from || '';
to = processingResult.email.to || [];
subject = processingResult.email.subject || '';
} else if (processingResult.from) {
from = processingResult.from;
to = processingResult.to || [];
subject = processingResult.subject || '';
}
}
items.push({
id: item.id,
processingMode: item.processingMode,
status: item.status,
attempts: item.attempts,
nextAttempt: item.nextAttempt instanceof Date ? item.nextAttempt.getTime() : item.nextAttempt,
lastError: item.lastError,
createdAt: item.createdAt instanceof Date ? item.createdAt.getTime() : item.createdAt,
updatedAt: item.updatedAt instanceof Date ? item.updatedAt.getTime() : item.updatedAt,
deliveredAt: item.deliveredAt instanceof Date ? item.deliveredAt.getTime() : item.deliveredAt,
from,
to,
subject,
});
}
// Sort by createdAt descending (newest first)
items.sort((a, b) => b.createdAt - a.createdAt);
// Apply pagination
return items.slice(offset, offset + limit);
}
}