UNPKG

@454creative/easy-email

Version:

A framework-agnostic email service library for Node.js with observability and monitoring

394 lines 16.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SesService = void 0; const client_ses_1 = require("@aws-sdk/client-ses"); const credential_providers_1 = require("@aws-sdk/credential-providers"); const email_options_interface_1 = require("../interfaces/email-options.interface"); const observability_service_1 = require("./observability.service"); /** * SES Email Service Implementation * Handles email sending through AWS SES with comprehensive error handling */ class SesService { constructor(config) { this.config = config; this.client = this.createSesClient(); this.observability = observability_service_1.ObservabilityService.getInstance(); } /** * Create SES client with proper configuration */ createSesClient() { const clientConfig = { region: this.config.region, apiVersion: this.config.apiVersion || '2010-12-01', maxAttempts: this.config.maxRetries || 3, }; // Configure credentials if (this.config.credentials) { clientConfig.credentials = this.config.credentials; } else if (this.config.accessKeyId && this.config.secretAccessKey) { clientConfig.credentials = { accessKeyId: this.config.accessKeyId, secretAccessKey: this.config.secretAccessKey, sessionToken: this.config.sessionToken, }; } else { // Use AWS credential providers chain clientConfig.credentials = (0, credential_providers_1.fromEnv)(); } // Configure endpoint if provided if (this.config.endpoint) { clientConfig.endpoint = this.config.endpoint; } // Configure HTTP options if (this.config.httpOptions) { clientConfig.requestHandler = { httpOptions: this.config.httpOptions, }; } return new client_ses_1.SESClient(clientConfig); } /** * Send email using SES */ sendEmail(options) { return __awaiter(this, void 0, void 0, function* () { const startTime = Date.now(); const eventId = this.observability.trackEmailAttempt(options, email_options_interface_1.EmailProviderType.SES, startTime); try { const command = this.buildSendEmailCommand(options); const result = yield this.client.send(command); const duration = Date.now() - startTime; const response = { success: true, messageId: result.MessageId, }; this.observability.trackEmailSuccess(eventId, response, duration, email_options_interface_1.EmailProviderType.SES); return response; } catch (error) { const duration = Date.now() - startTime; const response = this.handleSesError(error, 'send email'); this.observability.trackEmailFailure(eventId, error, duration, email_options_interface_1.EmailProviderType.SES); // Track SES-specific error if available if (error.name) { this.observability.trackSesError(error, error.name, duration); } return response; } }); } /** * Build SES SendEmailCommand from EmailOptions */ buildSendEmailCommand(options) { var _a, _b; const input = { Source: this.formatEmailAddress(options.from), Destination: { ToAddresses: this.formatRecipients(options.to), CcAddresses: options.cc ? this.formatRecipients(options.cc) : undefined, BccAddresses: options.bcc ? this.formatRecipients(options.bcc) : undefined, }, Message: { Subject: { Data: options.subject, Charset: 'UTF-8', }, Body: this.buildMessageBody(options), }, }; // Add reply-to if specified if (options.replyTo) { input.ReplyToAddresses = [options.replyTo]; } // Add configuration set if specified in headers if ((_a = options.headers) === null || _a === void 0 ? void 0 : _a.configurationSetName) { input.ConfigurationSetName = options.headers.configurationSetName; } // Add tags if specified in headers if ((_b = options.headers) === null || _b === void 0 ? void 0 : _b.tags) { try { const tags = JSON.parse(options.headers.tags); if (Array.isArray(tags)) { input.Tags = tags.map(tag => ({ Name: tag.name || tag.Name, Value: tag.value || tag.Value, })); } } catch (error) { // Ignore invalid tags format } } return new client_ses_1.SendEmailCommand(input); } /** * Build message body for SES */ buildMessageBody(options) { const body = {}; if (options.text) { body.Text = { Data: options.text, Charset: 'UTF-8', }; } if (options.html) { body.Html = { Data: options.html, Charset: 'UTF-8', }; } // SES doesn't support attachments in the same way as SMTP/SendGrid // Attachments would need to be handled differently (e.g., as links or embedded content) if (options.attachments && options.attachments.length > 0) { console.warn('SES attachments are not supported in this implementation. Consider using S3 links or embedding content.'); } return body; } /** * Format email address for SES */ formatEmailAddress(recipient) { if (typeof recipient === 'string') { return recipient; } return recipient.name ? `${recipient.name} <${recipient.email}>` : recipient.email; } /** * Format recipients for SES */ formatRecipients(recipients) { if (Array.isArray(recipients)) { return recipients.map(recipient => this.formatEmailAddress(recipient)); } return [this.formatEmailAddress(recipients)]; } /** * Handle SES-specific errors */ handleSesError(error, operation) { var _a, _b, _c, _d; console.error(`SES ${operation} failed:`, error); let errorMessage = `SES ${operation} failed`; let errorCode = 'SES_ERROR'; let provider = email_options_interface_1.EmailProviderType.SES; // Handle specific AWS SES errors if (error.name) { switch (error.name) { case 'MessageRejected': errorMessage = 'Email message was rejected by SES'; errorCode = 'MESSAGE_REJECTED'; break; case 'MailFromDomainNotVerified': errorMessage = 'Sender domain is not verified in SES'; errorCode = 'DOMAIN_NOT_VERIFIED'; break; case 'ConfigurationSetDoesNotExist': errorMessage = 'SES configuration set does not exist'; errorCode = 'CONFIGURATION_SET_NOT_FOUND'; break; case 'TemplateDoesNotExist': errorMessage = 'SES template does not exist'; errorCode = 'TEMPLATE_NOT_FOUND'; break; case 'AccountSendingPaused': errorMessage = 'SES account sending is paused'; errorCode = 'ACCOUNT_PAUSED'; break; case 'SendingPaused': errorMessage = 'SES sending is paused'; errorCode = 'SENDING_PAUSED'; break; case 'MessageTooLarge': errorMessage = 'Email message is too large for SES'; errorCode = 'MESSAGE_TOO_LARGE'; break; case 'InvalidParameterValue': errorMessage = 'Invalid parameter value provided to SES'; errorCode = 'INVALID_PARAMETER'; break; case 'InvalidParameter': errorMessage = 'Invalid parameter provided to SES'; errorCode = 'INVALID_PARAMETER'; break; case 'ValidationError': errorMessage = 'SES validation error'; errorCode = 'VALIDATION_ERROR'; break; case 'ThrottlingException': errorMessage = 'SES rate limit exceeded'; errorCode = 'RATE_LIMIT_EXCEEDED'; break; case 'ServiceUnavailable': errorMessage = 'SES service is temporarily unavailable'; errorCode = 'SERVICE_UNAVAILABLE'; break; case 'InternalFailure': errorMessage = 'SES internal error'; errorCode = 'INTERNAL_ERROR'; break; case 'NetworkError': errorMessage = 'Network error connecting to SES'; errorCode = 'NETWORK_ERROR'; break; case 'TimeoutError': errorMessage = 'SES request timed out'; errorCode = 'TIMEOUT_ERROR'; break; default: errorMessage = error.message || `SES ${operation} failed`; errorCode = error.name || 'SES_ERROR'; } } return { success: false, error: { message: errorMessage, code: errorCode, provider, details: { sesResponse: { name: error.name, message: error.message, code: (_a = error.$metadata) === null || _a === void 0 ? void 0 : _a.httpStatusCode, requestId: (_b = error.$metadata) === null || _b === void 0 ? void 0 : _b.requestId, cfId: (_c = error.$metadata) === null || _c === void 0 ? void 0 : _c.cfId, extendedRequestId: (_d = error.$metadata) === null || _d === void 0 ? void 0 : _d.extendedRequestId, }, }, }, }; } /** * Verify SES connection */ verifyConnection() { return __awaiter(this, void 0, void 0, function* () { const startTime = Date.now(); try { // SES doesn't have a direct "verify" method like SMTP // We'll try to get the sending quota as a connection test const { GetSendQuotaCommand } = yield Promise.resolve().then(() => __importStar(require('@aws-sdk/client-ses'))); const command = new GetSendQuotaCommand({}); yield this.client.send(command); const duration = Date.now() - startTime; this.observability.trackConnectionVerification(email_options_interface_1.EmailProviderType.SES, true, duration); return true; } catch (error) { const duration = Date.now() - startTime; this.observability.trackConnectionVerification(email_options_interface_1.EmailProviderType.SES, false, duration); console.error('SES connection verification failed:', error); return false; } }); } /** * Get SES sending statistics */ getSendingStatistics() { return __awaiter(this, void 0, void 0, function* () { try { const { GetSendStatisticsCommand } = yield Promise.resolve().then(() => __importStar(require('@aws-sdk/client-ses'))); const command = new GetSendStatisticsCommand({}); const result = yield this.client.send(command); // Track statistics in observability if (result.SendDataPoints && result.SendDataPoints.length > 0) { const latestStats = result.SendDataPoints[0]; this.observability.trackSesStatistics({ deliveryAttempts: latestStats.DeliveryAttempts || 0, bounces: latestStats.Bounces || 0, complaints: latestStats.Complaints || 0, rejects: latestStats.Rejects || 0, }); } return result.SendDataPoints; } catch (error) { console.error('Failed to get SES sending statistics:', error); return null; } }); } /** * Get SES sending quota */ getSendingQuota() { return __awaiter(this, void 0, void 0, function* () { try { const { GetSendQuotaCommand } = yield Promise.resolve().then(() => __importStar(require('@aws-sdk/client-ses'))); const command = new GetSendQuotaCommand({}); const result = yield this.client.send(command); const quotaData = { max24HourSend: result.Max24HourSend || 0, sentLast24Hours: result.SentLast24Hours || 0, maxSendRate: result.MaxSendRate || 0, }; // Track quota usage in observability this.observability.trackSesQuotaUsage(quotaData); return quotaData; } catch (error) { console.error('Failed to get SES sending quota:', error); return null; } }); } /** * Destroy SES client */ destroy() { if (this.client) { this.client.destroy(); } } } exports.SesService = SesService; //# sourceMappingURL=ses.service.js.map