956 lines
24 KiB
TypeScript
956 lines
24 KiB
TypeScript
import * as plugins from '../plugins.js';
|
|
import { Email } from './classes.email.js';
|
|
import type { IEmailOptions } from './classes.email.js';
|
|
import { DeliveryStatus } from './classes.emailsendjob.js';
|
|
import type { MtaService } from './classes.mta.js';
|
|
import type { IDnsRecord } from './classes.dnsmanager.js';
|
|
|
|
/**
|
|
* Authentication options for API requests
|
|
*/
|
|
interface AuthOptions {
|
|
/** Required API keys for different endpoints */
|
|
apiKeys: Map<string, string[]>;
|
|
/** JWT secret for token-based authentication */
|
|
jwtSecret?: string;
|
|
/** Whether to validate IP addresses */
|
|
validateIp?: boolean;
|
|
/** Allowed IP addresses */
|
|
allowedIps?: string[];
|
|
}
|
|
|
|
/**
|
|
* Rate limiting options for API endpoints
|
|
*/
|
|
interface RateLimitOptions {
|
|
/** Maximum requests per window */
|
|
maxRequests: number;
|
|
/** Time window in milliseconds */
|
|
windowMs: number;
|
|
/** Whether to apply per endpoint */
|
|
perEndpoint?: boolean;
|
|
/** Whether to apply per IP */
|
|
perIp?: boolean;
|
|
}
|
|
|
|
/**
|
|
* API route definition
|
|
*/
|
|
interface ApiRoute {
|
|
/** HTTP method */
|
|
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
/** Path pattern */
|
|
path: string;
|
|
/** Handler function */
|
|
handler: (req: any, res: any) => Promise<any>;
|
|
/** Required authentication level */
|
|
authLevel: 'none' | 'basic' | 'admin';
|
|
/** Rate limiting options */
|
|
rateLimit?: RateLimitOptions;
|
|
/** Route description */
|
|
description?: string;
|
|
}
|
|
|
|
/**
|
|
* Email send request
|
|
*/
|
|
interface SendEmailRequest {
|
|
/** Email details */
|
|
email: IEmailOptions;
|
|
/** Whether to validate domains before sending */
|
|
validateDomains?: boolean;
|
|
/** Priority level (1-5, 1 = highest) */
|
|
priority?: number;
|
|
}
|
|
|
|
/**
|
|
* Email status response
|
|
*/
|
|
interface EmailStatusResponse {
|
|
/** Email ID */
|
|
id: string;
|
|
/** Current status */
|
|
status: DeliveryStatus;
|
|
/** Send time */
|
|
sentAt?: Date;
|
|
/** Delivery time */
|
|
deliveredAt?: Date;
|
|
/** Error message if failed */
|
|
error?: string;
|
|
/** Recipient address */
|
|
recipient: string;
|
|
/** Number of delivery attempts */
|
|
attempts: number;
|
|
/** Next retry time */
|
|
nextRetry?: Date;
|
|
}
|
|
|
|
/**
|
|
* Domain verification response
|
|
*/
|
|
interface DomainVerificationResponse {
|
|
/** Domain name */
|
|
domain: string;
|
|
/** Whether the domain is verified */
|
|
verified: boolean;
|
|
/** Verification details */
|
|
details: {
|
|
/** SPF record status */
|
|
spf: {
|
|
valid: boolean;
|
|
record?: string;
|
|
error?: string;
|
|
};
|
|
/** DKIM record status */
|
|
dkim: {
|
|
valid: boolean;
|
|
record?: string;
|
|
error?: string;
|
|
};
|
|
/** DMARC record status */
|
|
dmarc: {
|
|
valid: boolean;
|
|
record?: string;
|
|
error?: string;
|
|
};
|
|
/** MX record status */
|
|
mx: {
|
|
valid: boolean;
|
|
records?: string[];
|
|
error?: string;
|
|
};
|
|
};
|
|
}
|
|
|
|
/**
|
|
* API error response
|
|
*/
|
|
interface ApiError {
|
|
/** Error code */
|
|
code: string;
|
|
/** Error message */
|
|
message: string;
|
|
/** Detailed error information */
|
|
details?: any;
|
|
}
|
|
|
|
/**
|
|
* Simple HTTP Response helper
|
|
*/
|
|
class HttpResponse {
|
|
private headers: Record<string, string> = {
|
|
'Content-Type': 'application/json'
|
|
};
|
|
public statusCode: number = 200;
|
|
|
|
constructor(private res: any) {}
|
|
|
|
header(name: string, value: string): HttpResponse {
|
|
this.headers[name] = value;
|
|
return this;
|
|
}
|
|
|
|
status(code: number): HttpResponse {
|
|
this.statusCode = code;
|
|
return this;
|
|
}
|
|
|
|
json(data: any): void {
|
|
this.res.writeHead(this.statusCode, this.headers);
|
|
this.res.end(JSON.stringify(data));
|
|
}
|
|
|
|
end(): void {
|
|
this.res.writeHead(this.statusCode, this.headers);
|
|
this.res.end();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* API Manager for MTA service
|
|
*/
|
|
export class ApiManager {
|
|
/** TypedRouter for API routing */
|
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
/** MTA service reference */
|
|
private mtaRef: MtaService;
|
|
/** HTTP server */
|
|
private server: any;
|
|
/** Authentication options */
|
|
private authOptions: AuthOptions;
|
|
/** API routes */
|
|
private routes: ApiRoute[] = [];
|
|
/** Rate limiters */
|
|
private rateLimiters: Map<string, {
|
|
count: number;
|
|
resetTime: number;
|
|
clients: Map<string, {
|
|
count: number;
|
|
resetTime: number;
|
|
}>;
|
|
}> = new Map();
|
|
|
|
/**
|
|
* Initialize API Manager
|
|
* @param mtaRef MTA service reference
|
|
*/
|
|
constructor(mtaRef?: MtaService) {
|
|
this.mtaRef = mtaRef;
|
|
|
|
// Default authentication options
|
|
this.authOptions = {
|
|
apiKeys: new Map(),
|
|
validateIp: false,
|
|
allowedIps: []
|
|
};
|
|
|
|
// Register routes
|
|
this.registerRoutes();
|
|
|
|
// Create HTTP server with request handler
|
|
this.server = plugins.http.createServer(this.handleRequest.bind(this));
|
|
}
|
|
|
|
/**
|
|
* Set MTA service reference
|
|
* @param mtaRef MTA service reference
|
|
*/
|
|
public setMtaService(mtaRef: MtaService): void {
|
|
this.mtaRef = mtaRef;
|
|
}
|
|
|
|
/**
|
|
* Configure authentication options
|
|
* @param options Authentication options
|
|
*/
|
|
public configureAuth(options: Partial<AuthOptions>): void {
|
|
this.authOptions = {
|
|
...this.authOptions,
|
|
...options
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Handle HTTP request
|
|
*/
|
|
private async handleRequest(req: any, res: any): Promise<void> {
|
|
const start = Date.now();
|
|
|
|
// Create a response helper
|
|
const response = new HttpResponse(res);
|
|
|
|
// Add CORS headers
|
|
response.header('Access-Control-Allow-Origin', '*');
|
|
response.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
response.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization, X-API-Key');
|
|
|
|
// Handle preflight OPTIONS request
|
|
if (req.method === 'OPTIONS') {
|
|
return response.status(200).end();
|
|
}
|
|
|
|
try {
|
|
// Parse URL to get path and query
|
|
const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
|
|
const path = url.pathname;
|
|
|
|
// Collect request body if POST or PUT
|
|
let body = '';
|
|
if (req.method === 'POST' || req.method === 'PUT') {
|
|
await new Promise<void>((resolve, reject) => {
|
|
req.on('data', (chunk: Buffer) => {
|
|
body += chunk.toString();
|
|
});
|
|
|
|
req.on('end', () => {
|
|
resolve();
|
|
});
|
|
|
|
req.on('error', (err: Error) => {
|
|
reject(err);
|
|
});
|
|
});
|
|
|
|
// Parse body as JSON if Content-Type is application/json
|
|
const contentType = req.headers['content-type'] || '';
|
|
if (contentType.includes('application/json')) {
|
|
try {
|
|
req.body = JSON.parse(body);
|
|
} catch (error) {
|
|
return response.status(400).json({
|
|
code: 'INVALID_JSON',
|
|
message: 'Invalid JSON in request body'
|
|
});
|
|
}
|
|
} else {
|
|
req.body = body;
|
|
}
|
|
}
|
|
|
|
// Add authentication level to request
|
|
req.authLevel = 'none';
|
|
|
|
// Check API key
|
|
const apiKey = req.headers['x-api-key'];
|
|
if (apiKey) {
|
|
for (const [level, keys] of this.authOptions.apiKeys.entries()) {
|
|
if (keys.includes(apiKey)) {
|
|
req.authLevel = level;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check JWT token (if configured)
|
|
if (this.authOptions.jwtSecret && req.headers.authorization) {
|
|
try {
|
|
const token = req.headers.authorization.split(' ')[1];
|
|
// Note: We would need to add JWT verification
|
|
// Using a simple placeholder for now
|
|
const decoded = { level: 'none' }; // Simplified - would use actual JWT library
|
|
|
|
if (decoded && decoded.level) {
|
|
req.authLevel = decoded.level;
|
|
req.user = decoded;
|
|
}
|
|
} catch (error) {
|
|
// Invalid token, but don't fail the request yet
|
|
console.error('Invalid JWT token:', error.message);
|
|
}
|
|
}
|
|
|
|
// Check IP address (if configured)
|
|
if (this.authOptions.validateIp) {
|
|
const clientIp = req.socket.remoteAddress;
|
|
if (!this.authOptions.allowedIps.includes(clientIp)) {
|
|
return response.status(403).json({
|
|
code: 'FORBIDDEN',
|
|
message: 'IP address not allowed'
|
|
});
|
|
}
|
|
}
|
|
|
|
// Find matching route
|
|
const route = this.findRoute(req.method, path);
|
|
|
|
if (!route) {
|
|
return response.status(404).json({
|
|
code: 'NOT_FOUND',
|
|
message: 'Endpoint not found'
|
|
});
|
|
}
|
|
|
|
// Check authentication
|
|
if (route.authLevel !== 'none' && req.authLevel !== route.authLevel && req.authLevel !== 'admin') {
|
|
return response.status(403).json({
|
|
code: 'FORBIDDEN',
|
|
message: `This endpoint requires ${route.authLevel} access`
|
|
});
|
|
}
|
|
|
|
// Check rate limit
|
|
if (route.rateLimit) {
|
|
const exceeded = this.checkRateLimit(route, req);
|
|
if (exceeded) {
|
|
return response.status(429).json({
|
|
code: 'RATE_LIMIT_EXCEEDED',
|
|
message: 'Rate limit exceeded, please try again later'
|
|
});
|
|
}
|
|
}
|
|
|
|
// Extract path parameters
|
|
const pathParams = this.extractPathParams(route.path, path);
|
|
req.params = pathParams;
|
|
|
|
// Extract query parameters
|
|
req.query = {};
|
|
for (const [key, value] of url.searchParams.entries()) {
|
|
req.query[key] = value;
|
|
}
|
|
|
|
// Handle the request
|
|
await route.handler(req, response);
|
|
|
|
// Log request
|
|
const duration = Date.now() - start;
|
|
console.log(`[API] ${req.method} ${path} ${response.statusCode} ${duration}ms`);
|
|
} catch (error) {
|
|
console.error(`Error handling request:`, error);
|
|
|
|
// Send appropriate error response
|
|
const status = error.status || 500;
|
|
const apiError: ApiError = {
|
|
code: error.code || 'INTERNAL_ERROR',
|
|
message: error.message || 'Internal server error'
|
|
};
|
|
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
apiError.details = error.stack;
|
|
}
|
|
|
|
response.status(status).json(apiError);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find a route matching the method and path
|
|
*/
|
|
private findRoute(method: string, path: string): ApiRoute | null {
|
|
for (const route of this.routes) {
|
|
if (route.method === method && this.pathMatches(route.path, path)) {
|
|
return route;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Check if a path matches a route pattern
|
|
*/
|
|
private pathMatches(pattern: string, path: string): boolean {
|
|
// Convert route pattern to regex
|
|
const patternParts = pattern.split('/');
|
|
const pathParts = path.split('/');
|
|
|
|
if (patternParts.length !== pathParts.length) {
|
|
return false;
|
|
}
|
|
|
|
for (let i = 0; i < patternParts.length; i++) {
|
|
if (patternParts[i].startsWith(':')) {
|
|
// Parameter - always matches
|
|
continue;
|
|
}
|
|
|
|
if (patternParts[i] !== pathParts[i]) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Extract path parameters from URL
|
|
*/
|
|
private extractPathParams(pattern: string, path: string): Record<string, string> {
|
|
const params: Record<string, string> = {};
|
|
const patternParts = pattern.split('/');
|
|
const pathParts = path.split('/');
|
|
|
|
for (let i = 0; i < patternParts.length; i++) {
|
|
if (patternParts[i].startsWith(':')) {
|
|
const paramName = patternParts[i].substring(1);
|
|
params[paramName] = pathParts[i];
|
|
}
|
|
}
|
|
|
|
return params;
|
|
}
|
|
|
|
/**
|
|
* Register API routes
|
|
*/
|
|
private registerRoutes(): void {
|
|
// Email routes
|
|
this.addRoute({
|
|
method: 'POST',
|
|
path: '/api/email/send',
|
|
handler: this.handleSendEmail.bind(this),
|
|
authLevel: 'basic',
|
|
description: 'Send an email'
|
|
});
|
|
|
|
this.addRoute({
|
|
method: 'GET',
|
|
path: '/api/email/status/:id',
|
|
handler: this.handleGetEmailStatus.bind(this),
|
|
authLevel: 'basic',
|
|
description: 'Get email delivery status'
|
|
});
|
|
|
|
// Domain routes
|
|
this.addRoute({
|
|
method: 'GET',
|
|
path: '/api/domain/verify/:domain',
|
|
handler: this.handleVerifyDomain.bind(this),
|
|
authLevel: 'basic',
|
|
description: 'Verify domain DNS records'
|
|
});
|
|
|
|
this.addRoute({
|
|
method: 'GET',
|
|
path: '/api/domain/records/:domain',
|
|
handler: this.handleGetDomainRecords.bind(this),
|
|
authLevel: 'basic',
|
|
description: 'Get recommended DNS records for domain'
|
|
});
|
|
|
|
// DKIM routes
|
|
this.addRoute({
|
|
method: 'POST',
|
|
path: '/api/dkim/generate/:domain',
|
|
handler: this.handleGenerateDkim.bind(this),
|
|
authLevel: 'admin',
|
|
description: 'Generate DKIM keys for domain'
|
|
});
|
|
|
|
this.addRoute({
|
|
method: 'GET',
|
|
path: '/api/dkim/public/:domain',
|
|
handler: this.handleGetDkimPublicKey.bind(this),
|
|
authLevel: 'basic',
|
|
description: 'Get DKIM public key for domain'
|
|
});
|
|
|
|
// Stats route
|
|
this.addRoute({
|
|
method: 'GET',
|
|
path: '/api/stats',
|
|
handler: this.handleGetStats.bind(this),
|
|
authLevel: 'admin',
|
|
description: 'Get MTA statistics'
|
|
});
|
|
|
|
// Documentation route
|
|
this.addRoute({
|
|
method: 'GET',
|
|
path: '/api',
|
|
handler: this.handleGetApiDocs.bind(this),
|
|
authLevel: 'none',
|
|
description: 'API documentation'
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Add an API route
|
|
* @param route Route definition
|
|
*/
|
|
private addRoute(route: ApiRoute): void {
|
|
this.routes.push(route);
|
|
}
|
|
|
|
/**
|
|
* Check rate limit for a route
|
|
* @param route Route definition
|
|
* @param req Express request
|
|
* @returns Whether rate limit is exceeded
|
|
*/
|
|
private checkRateLimit(route: ApiRoute, req: any): boolean {
|
|
if (!route.rateLimit) return false;
|
|
|
|
const { maxRequests, windowMs, perEndpoint, perIp } = route.rateLimit;
|
|
|
|
// Determine rate limit key
|
|
let key = 'global';
|
|
if (perEndpoint) {
|
|
key = `${route.method}:${route.path}`;
|
|
}
|
|
|
|
// Get or create limiter
|
|
if (!this.rateLimiters.has(key)) {
|
|
this.rateLimiters.set(key, {
|
|
count: 0,
|
|
resetTime: Date.now() + windowMs,
|
|
clients: new Map()
|
|
});
|
|
}
|
|
|
|
const limiter = this.rateLimiters.get(key);
|
|
|
|
// Reset if window has passed
|
|
if (Date.now() > limiter.resetTime) {
|
|
limiter.count = 0;
|
|
limiter.resetTime = Date.now() + windowMs;
|
|
limiter.clients.clear();
|
|
}
|
|
|
|
// Check per-IP limit if enabled
|
|
if (perIp) {
|
|
const clientIp = req.socket.remoteAddress;
|
|
let clientLimiter = limiter.clients.get(clientIp);
|
|
|
|
if (!clientLimiter) {
|
|
clientLimiter = {
|
|
count: 0,
|
|
resetTime: Date.now() + windowMs
|
|
};
|
|
limiter.clients.set(clientIp, clientLimiter);
|
|
}
|
|
|
|
// Reset client limiter if needed
|
|
if (Date.now() > clientLimiter.resetTime) {
|
|
clientLimiter.count = 0;
|
|
clientLimiter.resetTime = Date.now() + windowMs;
|
|
}
|
|
|
|
// Check client limit
|
|
if (clientLimiter.count >= maxRequests) {
|
|
return true;
|
|
}
|
|
|
|
// Increment client count
|
|
clientLimiter.count++;
|
|
} else {
|
|
// Check global limit
|
|
if (limiter.count >= maxRequests) {
|
|
return true;
|
|
}
|
|
|
|
// Increment global count
|
|
limiter.count++;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Create an API error
|
|
* @param code Error code
|
|
* @param message Error message
|
|
* @param status HTTP status code
|
|
* @param details Additional details
|
|
* @returns API error
|
|
*/
|
|
private createError(code: string, message: string, status = 400, details?: any): Error & { code: string; status: number; details?: any } {
|
|
const error = new Error(message) as Error & { code: string; status: number; details?: any };
|
|
error.code = code;
|
|
error.status = status;
|
|
if (details) {
|
|
error.details = details;
|
|
}
|
|
return error;
|
|
}
|
|
|
|
/**
|
|
* Validate that MTA service is available
|
|
*/
|
|
private validateMtaService(): void {
|
|
if (!this.mtaRef) {
|
|
throw this.createError('SERVICE_UNAVAILABLE', 'MTA service is not available', 503);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle email send request
|
|
* @param req Express request
|
|
* @param res Express response
|
|
*/
|
|
private async handleSendEmail(req: any, res: any): Promise<void> {
|
|
this.validateMtaService();
|
|
|
|
const data = req.body as SendEmailRequest;
|
|
|
|
if (!data || !data.email) {
|
|
throw this.createError('INVALID_REQUEST', 'Missing email data');
|
|
}
|
|
|
|
try {
|
|
// Create Email instance
|
|
const email = new Email(data.email);
|
|
|
|
// Validate domains if requested
|
|
if (data.validateDomains) {
|
|
const fromDomain = email.getFromDomain();
|
|
if (fromDomain) {
|
|
const verification = await this.mtaRef.dnsManager.verifyEmailAuthRecords(fromDomain);
|
|
|
|
// Check if SPF and DKIM are valid
|
|
if (!verification.spf.valid || !verification.dkim.valid) {
|
|
throw this.createError('DOMAIN_VERIFICATION_FAILED', 'Domain DNS verification failed', 400, {
|
|
verification
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Send email
|
|
const id = await this.mtaRef.send(email);
|
|
|
|
// Return success response
|
|
res.json({
|
|
id,
|
|
message: 'Email queued successfully',
|
|
status: 'pending'
|
|
});
|
|
} catch (error) {
|
|
// Handle Email constructor errors
|
|
if (error.message.includes('Invalid') || error.message.includes('must have')) {
|
|
throw this.createError('INVALID_EMAIL', error.message);
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle email status request
|
|
* @param req Express request
|
|
* @param res Express response
|
|
*/
|
|
private async handleGetEmailStatus(req: any, res: any): Promise<void> {
|
|
this.validateMtaService();
|
|
|
|
const id = req.params.id;
|
|
|
|
if (!id) {
|
|
throw this.createError('INVALID_REQUEST', 'Missing email ID');
|
|
}
|
|
|
|
// Get email status
|
|
const status = this.mtaRef.getEmailStatus(id);
|
|
|
|
if (!status) {
|
|
throw this.createError('NOT_FOUND', `Email with ID ${id} not found`, 404);
|
|
}
|
|
|
|
// Create response
|
|
const response: EmailStatusResponse = {
|
|
id: status.id,
|
|
status: status.status,
|
|
sentAt: status.addedAt,
|
|
recipient: status.email.to[0],
|
|
attempts: status.attempts
|
|
};
|
|
|
|
// Add additional fields if available
|
|
if (status.lastAttempt) {
|
|
response.sentAt = status.lastAttempt;
|
|
}
|
|
|
|
if (status.status === DeliveryStatus.DELIVERED) {
|
|
response.deliveredAt = status.lastAttempt;
|
|
}
|
|
|
|
if (status.error) {
|
|
response.error = status.error.message;
|
|
}
|
|
|
|
if (status.nextAttempt) {
|
|
response.nextRetry = status.nextAttempt;
|
|
}
|
|
|
|
res.json(response);
|
|
}
|
|
|
|
/**
|
|
* Handle domain verification request
|
|
* @param req Express request
|
|
* @param res Express response
|
|
*/
|
|
private async handleVerifyDomain(req: any, res: any): Promise<void> {
|
|
this.validateMtaService();
|
|
|
|
const domain = req.params.domain;
|
|
|
|
if (!domain) {
|
|
throw this.createError('INVALID_REQUEST', 'Missing domain');
|
|
}
|
|
|
|
try {
|
|
// Verify domain DNS records
|
|
const records = await this.mtaRef.dnsManager.verifyEmailAuthRecords(domain);
|
|
|
|
// Get MX records
|
|
let mxValid = false;
|
|
let mxRecords: string[] = [];
|
|
let mxError: string = undefined;
|
|
|
|
try {
|
|
const mxResult = await this.mtaRef.dnsManager.lookupMx(domain);
|
|
mxValid = mxResult.length > 0;
|
|
mxRecords = mxResult.map(mx => mx.exchange);
|
|
} catch (error) {
|
|
mxError = error.message;
|
|
}
|
|
|
|
// Create response
|
|
const response: DomainVerificationResponse = {
|
|
domain,
|
|
verified: records.spf.valid && records.dkim.valid && records.dmarc.valid && mxValid,
|
|
details: {
|
|
spf: {
|
|
valid: records.spf.valid,
|
|
record: records.spf.value,
|
|
error: records.spf.error
|
|
},
|
|
dkim: {
|
|
valid: records.dkim.valid,
|
|
record: records.dkim.value,
|
|
error: records.dkim.error
|
|
},
|
|
dmarc: {
|
|
valid: records.dmarc.valid,
|
|
record: records.dmarc.value,
|
|
error: records.dmarc.error
|
|
},
|
|
mx: {
|
|
valid: mxValid,
|
|
records: mxRecords,
|
|
error: mxError
|
|
}
|
|
}
|
|
};
|
|
|
|
res.json(response);
|
|
} catch (error) {
|
|
throw this.createError('VERIFICATION_FAILED', `Domain verification failed: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle get domain records request
|
|
* @param req Express request
|
|
* @param res Express response
|
|
*/
|
|
private async handleGetDomainRecords(req: any, res: any): Promise<void> {
|
|
this.validateMtaService();
|
|
|
|
const domain = req.params.domain;
|
|
|
|
if (!domain) {
|
|
throw this.createError('INVALID_REQUEST', 'Missing domain');
|
|
}
|
|
|
|
try {
|
|
// Generate recommended DNS records
|
|
const records = await this.mtaRef.dnsManager.generateAllRecommendedRecords(domain);
|
|
|
|
res.json({
|
|
domain,
|
|
records
|
|
});
|
|
} catch (error) {
|
|
throw this.createError('GENERATION_FAILED', `DNS record generation failed: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle generate DKIM keys request
|
|
* @param req Express request
|
|
* @param res Express response
|
|
*/
|
|
private async handleGenerateDkim(req: any, res: any): Promise<void> {
|
|
this.validateMtaService();
|
|
|
|
const domain = req.params.domain;
|
|
|
|
if (!domain) {
|
|
throw this.createError('INVALID_REQUEST', 'Missing domain');
|
|
}
|
|
|
|
try {
|
|
// Generate DKIM keys
|
|
await this.mtaRef.dkimCreator.handleDKIMKeysForDomain(domain);
|
|
|
|
// Get DNS record
|
|
const dnsRecord = await this.mtaRef.dkimCreator.getDNSRecordForDomain(domain);
|
|
|
|
res.json({
|
|
domain,
|
|
dnsRecord,
|
|
message: 'DKIM keys generated successfully'
|
|
});
|
|
} catch (error) {
|
|
throw this.createError('GENERATION_FAILED', `DKIM generation failed: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle get DKIM public key request
|
|
* @param req Express request
|
|
* @param res Express response
|
|
*/
|
|
private async handleGetDkimPublicKey(req: any, res: any): Promise<void> {
|
|
this.validateMtaService();
|
|
|
|
const domain = req.params.domain;
|
|
|
|
if (!domain) {
|
|
throw this.createError('INVALID_REQUEST', 'Missing domain');
|
|
}
|
|
|
|
try {
|
|
// Get DKIM keys
|
|
const keys = await this.mtaRef.dkimCreator.readDKIMKeys(domain);
|
|
|
|
// Get DNS record
|
|
const dnsRecord = await this.mtaRef.dkimCreator.getDNSRecordForDomain(domain);
|
|
|
|
res.json({
|
|
domain,
|
|
publicKey: keys.publicKey,
|
|
dnsRecord
|
|
});
|
|
} catch (error) {
|
|
throw this.createError('NOT_FOUND', `DKIM keys not found for domain: ${domain}`, 404);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle get stats request
|
|
* @param req Express request
|
|
* @param res Express response
|
|
*/
|
|
private async handleGetStats(req: any, res: any): Promise<void> {
|
|
this.validateMtaService();
|
|
|
|
// Get MTA stats
|
|
const stats = this.mtaRef.getStats();
|
|
|
|
res.json(stats);
|
|
}
|
|
|
|
/**
|
|
* Handle get API docs request
|
|
* @param req Express request
|
|
* @param res Express response
|
|
*/
|
|
private async handleGetApiDocs(req: any, res: any): Promise<void> {
|
|
// Generate API documentation
|
|
const docs = {
|
|
name: 'MTA API',
|
|
version: '1.0.0',
|
|
description: 'API for interacting with the MTA service',
|
|
endpoints: this.routes.map(route => ({
|
|
method: route.method,
|
|
path: route.path,
|
|
description: route.description,
|
|
authLevel: route.authLevel
|
|
}))
|
|
};
|
|
|
|
res.json(docs);
|
|
}
|
|
|
|
/**
|
|
* Start the API server
|
|
* @param port Port to listen on
|
|
* @returns Promise that resolves when server is started
|
|
*/
|
|
public start(port: number = 3000): Promise<void> {
|
|
return new Promise((resolve, reject) => {
|
|
try {
|
|
// Start HTTP server
|
|
this.server.listen(port, () => {
|
|
console.log(`API server listening on port ${port}`);
|
|
resolve();
|
|
});
|
|
} catch (error) {
|
|
console.error('Failed to start API server:', error);
|
|
reject(error);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Stop the API server
|
|
*/
|
|
public stop(): void {
|
|
if (this.server) {
|
|
this.server.close();
|
|
console.log('API server stopped');
|
|
}
|
|
}
|
|
} |