@454creative/easy-email
Version:
A framework-agnostic email service library for Node.js with observability and monitoring
394 lines • 16.4 kB
JavaScript
"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