326 lines
10 KiB
TypeScript
326 lines
10 KiB
TypeScript
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);
|
|
}
|
|
}
|