cakemail-mcp-server
Version:
Enterprise MCP server for Cakemail API integration with Claude AI - includes comprehensive template management, list management, sub-account management, BEEeditor visual email design, and advanced analytics
576 lines • 24.6 kB
JavaScript
// Email API operations - v2 API for both transactional and marketing emails
import { BaseApiClient } from './base-client.js';
import { EmailAPIError } from '../types/errors.js';
import logger from '../utils/logger.js';
export class EmailApi extends BaseApiClient {
/**
* Submit an email to be sent using v2 API
* Fully compliant with POST /v2/emails specification
*/
async sendEmail(data) {
const emailData = data;
// Enhanced validation
if (!this.isValidEmail(emailData.email)) {
throw EmailAPIError.forInvalidEmail(emailData.email);
}
if (!emailData.sender?.id) {
throw new EmailAPIError('sender.id is required', 400);
}
if (!emailData.content?.subject) {
throw new EmailAPIError('content.subject is required', 400);
}
// Must have either content or template
if (!emailData.content.html && !emailData.content.text && !emailData.content.template?.id) {
throw EmailAPIError.forMissingContent();
}
// For marketing emails or accounts that require list management, ensure list_id is provided
if (emailData.content.type === 'marketing' && !emailData.list_id) {
throw new EmailAPIError('list_id is required for marketing emails', 400);
}
// Some accounts require list_id for all emails (list management cannot be disabled)
// This is a common requirement for newer accounts or certain account types
if (!emailData.list_id && emailData.content.type !== 'transactional') {
logger.info('[Email API] Warning: Some accounts require list_id for all emails. Consider providing list_id.');
}
// Structure request according to v2 API specification
// Based on the OpenAPI schema, the structure should match the SubmitEmail schema
const submitRequest = {
sender: {
id: emailData.sender.id
},
content: {
subject: emailData.content.subject
},
email: emailData.email
};
// Add content fields conditionally
if (emailData.content.html) {
submitRequest.content.html = emailData.content.html;
}
if (emailData.content.text) {
submitRequest.content.text = emailData.content.text;
}
if (emailData.content.template?.id) {
submitRequest.content.template = { id: emailData.content.template.id };
}
// Set encoding - required when html or text content is present
if (emailData.content.html || emailData.content.text) {
submitRequest.content.encoding = emailData.content.encoding || 'utf-8';
}
else if (emailData.content.encoding) {
submitRequest.content.encoding = emailData.content.encoding;
}
if (emailData.content.custom_attributes) {
submitRequest.content.custom_attributes = emailData.content.custom_attributes;
}
if (emailData.content.type) {
submitRequest.content.type = emailData.content.type;
}
if (emailData.content.markup) {
submitRequest.content.markup = emailData.content.markup;
}
// Add optional sender name
if (emailData.sender.name) {
submitRequest.sender.name = emailData.sender.name;
}
// Add optional top-level fields
if (emailData.list_id !== undefined) {
submitRequest.list_id = emailData.list_id;
}
if (emailData.contact_id !== undefined) {
submitRequest.contact_id = emailData.contact_id;
}
if (emailData.tags && Array.isArray(emailData.tags)) {
submitRequest.tags = emailData.tags;
}
if (emailData.tracking) {
submitRequest.tracking = emailData.tracking;
}
if (emailData.additional_headers && Array.isArray(emailData.additional_headers)) {
submitRequest.additional_headers = emailData.additional_headers;
}
if (emailData.attachment && Array.isArray(emailData.attachment)) {
submitRequest.attachment = emailData.attachment;
}
if (this.debugMode) {
logger.info('[Email API] v2 Submit request:', JSON.stringify(submitRequest, null, 2));
}
const accountId = await this.getCurrentAccountId();
const query = accountId ? `?account_id=${accountId}` : '';
try {
const response = await this.makeRequest(`/v2/emails${query}`, {
method: 'POST',
body: JSON.stringify(submitRequest)
});
return response;
}
catch (error) {
// Enhanced error handling with better details
if (error instanceof Error) {
const errorMessage = error.message;
let detailedMessage = `Failed to send email: ${errorMessage}`;
// Extract more details from the error if available
if (error.response) {
try {
const responseDetails = JSON.stringify(error.response, null, 2);
detailedMessage += `\n\nAPI Response Details:\n${responseDetails}`;
}
catch {
detailedMessage += `\n\nAPI Response: ${String(error.response)}`;
}
}
// If it's a CakemailError, extract more specific details
if (error.statusCode && error.response) {
const statusCode = error.statusCode;
const response = error.response;
if (response && typeof response === 'object') {
// Handle FastAPI validation errors (Pydantic format)
if (Array.isArray(response.detail)) {
const validationErrors = response.detail.map((err) => {
const field = Array.isArray(err.loc) ? err.loc.join('.') : 'unknown';
return `${field}: ${err.msg}`;
}).join(', ');
detailedMessage = `Failed to send email (${statusCode}): Validation errors - ${validationErrors}`;
// Provide helpful suggestions for common validation errors
if (validationErrors.includes('content.encoding is required')) {
detailedMessage += '\n\nSuggestion: Add encoding: "utf-8" to your content when using html or text.';
}
if (validationErrors.includes('List Management') || detailedMessage.includes('list_id')) {
detailedMessage += '\n\nSuggestion: This account requires a list_id. Please provide a valid list_id parameter.';
}
}
// Handle simple error messages
else if (response.detail && typeof response.detail === 'string') {
detailedMessage = `Failed to send email (${statusCode}): ${response.detail}`;
// Provide helpful suggestions for common error messages
if (response.detail.includes('List Management')) {
detailedMessage += '\n\nSuggestion: This account requires a list_id. Please provide a valid list_id parameter.';
}
}
// Handle generic error objects
else if (response.message) {
detailedMessage = `Failed to send email (${statusCode}): ${response.message}`;
}
// Handle error field
else if (response.error) {
detailedMessage = `Failed to send email (${statusCode}): ${response.error}`;
}
// Fallback to JSON serialization
else {
try {
detailedMessage = `Failed to send email (${statusCode}): ${JSON.stringify(response)}`;
}
catch {
detailedMessage = `Failed to send email (${statusCode}): Error parsing response`;
}
}
}
}
if (this.debugMode) {
logger.error('[Email API] Detailed error:', {
message: errorMessage,
error: error,
response: error.response,
submitRequest: submitRequest
});
}
throw new EmailAPIError(detailedMessage, 500);
}
throw error;
}
}
/**
* Retrieve a submitted email status
* Compliant with GET /v2/emails/{email_id} specification
*/
async getEmail(emailId) {
if (!emailId) {
throw new EmailAPIError('email_id is required', 400);
}
// Validate UUID format (v4 UUID)
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
if (!uuidRegex.test(emailId)) {
throw new EmailAPIError('email_id must be a valid UUID', 400);
}
const accountId = await this.getCurrentAccountId();
const query = accountId ? `?account_id=${accountId}` : '';
try {
const response = await this.makeRequest(`/v2/emails/${emailId}${query}`);
return response;
}
catch (error) {
if (error instanceof Error) {
throw new EmailAPIError(`Failed to retrieve email ${emailId}: ${error.message}`, 500, emailId);
}
throw error;
}
}
/**
* Render a submitted email (get HTML/text content)
*/
async renderEmail(emailId, options = {}) {
if (!emailId) {
throw new EmailAPIError('email_id is required', 400);
}
// Validate UUID format
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
if (!uuidRegex.test(emailId)) {
throw new EmailAPIError('email_id must be a valid UUID', 400);
}
const accountId = await this.getCurrentAccountId();
const queryParams = new URLSearchParams();
if (accountId) {
queryParams.append('account_id', String(accountId));
}
if (options.as_submitted !== undefined) {
queryParams.append('as_submitted', String(options.as_submitted));
}
if (options.tracking !== undefined) {
queryParams.append('tracking', String(options.tracking));
}
const query = queryParams.toString() ? `?${queryParams.toString()}` : '';
try {
return await this.makeRequest(`/v2/emails/${emailId}/render${query}`, {
headers: {
'Accept': 'text/html'
}
});
}
catch (error) {
if (error instanceof Error) {
throw new EmailAPIError(`Failed to render email ${emailId}: ${error.message}`, 500, emailId);
}
throw error;
}
}
/**
* Show Email API activity logs
* Compliant with GET /v2/logs/emails specification
*/
async getEmailLogs(options = {}) {
const accountId = await this.getCurrentAccountId();
const queryParams = new URLSearchParams();
if (accountId) {
queryParams.append('account_id', String(accountId));
}
if (options.log_type) {
queryParams.append('log_type', options.log_type);
}
if (options.email_id) {
// Validate UUID format for email_id
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
if (!uuidRegex.test(options.email_id)) {
throw new EmailAPIError('email_id must be a valid UUID format', 400);
}
queryParams.append('email_id', options.email_id);
}
if (options.iso_time !== undefined) {
queryParams.append('iso_time', String(options.iso_time));
}
if (options.page) {
if (options.page < 1) {
throw new EmailAPIError('page must be >= 1', 400);
}
queryParams.append('page', String(options.page));
}
if (options.per_page) {
if (options.per_page < 1 || options.per_page > 100) {
throw new EmailAPIError('per_page must be between 1 and 100', 400);
}
queryParams.append('per_page', String(options.per_page));
}
if (options.start_time) {
if (options.start_time < 1 || options.start_time > 2147483647) {
throw new EmailAPIError('start_time must be a valid Unix timestamp', 400);
}
queryParams.append('start_time', String(options.start_time));
}
if (options.end_time) {
if (options.end_time < 1 || options.end_time > 2147483647) {
throw new EmailAPIError('end_time must be a valid Unix timestamp', 400);
}
queryParams.append('end_time', String(options.end_time));
}
if (options.tags) {
// Validate JSON format
try {
JSON.parse(options.tags);
}
catch {
throw new EmailAPIError('tags must be valid JSON', 400);
}
queryParams.append('tags', options.tags);
}
if (options.providers) {
// Validate JSON format
try {
JSON.parse(options.providers);
}
catch {
throw new EmailAPIError('providers must be valid JSON', 400);
}
queryParams.append('providers', options.providers);
}
if (options.sort) {
const validSortFields = ['id', 'time', 'submitted_time', 'type', 'provider'];
const sortField = options.sort.replace(/^[-+]/, ''); // Remove direction prefix
if (!validSortFields.includes(sortField)) {
throw new EmailAPIError(`sort field must be one of: ${validSortFields.join(', ')}`, 400);
}
queryParams.append('sort', options.sort);
}
const query = queryParams.toString() ? `?${queryParams.toString()}` : '';
try {
const response = await this.makeRequest(`/v2/logs/emails${query}`);
return response;
}
catch (error) {
if (error instanceof Error) {
throw new EmailAPIError(`Failed to retrieve email logs: ${error.message}`, 500);
}
throw error;
}
}
/**
* Show Email API statistics
* Compliant with GET /v2/reports/emails specification
*/
async getEmailStats(options = {}) {
const accountId = await this.getCurrentAccountId();
const queryParams = new URLSearchParams();
if (accountId) {
queryParams.append('account_id', String(accountId));
}
if (options.interval) {
const validIntervals = ['hour', 'day', 'week', 'month'];
if (!validIntervals.includes(options.interval)) {
throw new EmailAPIError(`interval must be one of: ${validIntervals.join(', ')}`, 400);
}
queryParams.append('interval', options.interval);
}
if (options.iso_time !== undefined) {
queryParams.append('iso_time', String(options.iso_time));
}
if (options.start_time) {
if (options.start_time < 1 || options.start_time > 2147483647) {
throw new EmailAPIError('start_time must be a valid Unix timestamp', 400);
}
queryParams.append('start_time', String(options.start_time));
}
if (options.end_time) {
if (options.end_time < 1 || options.end_time > 2147483647) {
throw new EmailAPIError('end_time must be a valid Unix timestamp', 400);
}
queryParams.append('end_time', String(options.end_time));
}
if (options.providers) {
// Validate JSON format
try {
JSON.parse(options.providers);
}
catch {
throw new EmailAPIError('providers must be valid JSON', 400);
}
queryParams.append('providers', options.providers);
}
if (options.tags) {
// Validate JSON format
try {
JSON.parse(options.tags);
}
catch {
throw new EmailAPIError('tags must be valid JSON', 400);
}
queryParams.append('tags', options.tags);
}
const query = queryParams.toString() ? `?${queryParams.toString()}` : '';
try {
const response = await this.makeRequest(`/v2/reports/emails${query}`);
return response;
}
catch (error) {
if (error instanceof Error) {
throw new EmailAPIError(`Failed to retrieve email statistics: ${error.message}`, 500);
}
throw error;
}
}
/**
* Helper method to send transactional email
*/
async sendTransactionalEmail(data) {
const emailData = { ...data };
emailData.content.type = 'transactional';
return this.sendEmail(emailData);
}
/**
* Helper method to send marketing email
*/
async sendMarketingEmail(data) {
const emailData = { ...data };
emailData.content.type = 'marketing';
return this.sendEmail(emailData);
}
/**
* Helper method to get email status (alias for getEmail)
*/
async getEmailStatus(emailId) {
return this.getEmail(emailId);
}
/**
* Bulk email status retrieval
*/
async getBulkEmailStatus(emailIds) {
if (!emailIds || emailIds.length === 0) {
throw new EmailAPIError('emailIds array cannot be empty', 400);
}
if (emailIds.length > 100) {
throw new EmailAPIError('Cannot retrieve more than 100 emails at once', 400);
}
// Validate all UUIDs
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
const invalidIds = emailIds.filter(id => !uuidRegex.test(id));
if (invalidIds.length > 0) {
throw new EmailAPIError(`Invalid UUID format for email IDs: ${invalidIds.join(', ')}`, 400);
}
const promises = emailIds.map(id => this.getEmail(id));
return Promise.all(promises);
}
/**
* Create a filter for logs/stats using the recursive filter syntax
* Enhanced with validation and helper patterns
*/
createFilter(conditions, operator = 'and') {
if (!conditions || conditions.length === 0) {
throw new EmailAPIError('Conditions are required for filter creation', 400);
}
const validOperators = ['and', 'or', 'not', 'is'];
if (!validOperators.includes(operator)) {
throw new EmailAPIError(`Operator must be one of: ${validOperators.join(', ')}`, 400);
}
let filter;
if (conditions.length === 1 && typeof conditions[0] === 'string') {
filter = { [operator]: conditions[0] };
}
else {
filter = { [operator]: conditions };
}
try {
return JSON.stringify(filter);
}
catch (error) {
throw new EmailAPIError('Failed to create valid JSON filter', 400);
}
}
/**
* Helper to create simple tag filters
*/
createTagFilter(tags, operator = 'or') {
if (!tags || tags.length === 0) {
throw new EmailAPIError('Tags array cannot be empty', 400);
}
return this.createFilter(tags, operator);
}
/**
* Helper to create provider filters
*/
createProviderFilter(providers, operator = 'or') {
if (!providers || providers.length === 0) {
throw new EmailAPIError('Providers array cannot be empty', 400);
}
return this.createFilter(providers, operator);
}
/**
* Create smart filters for common use cases
*/
createSmartFilter(filterType) {
const smartFilters = {
engagement: ['click', 'open', 'view', 'forward', 'share'],
critical_issues: ['spam', 'bounce_hb', 'bounce_mb'],
temporary_failures: ['bounce_sb', 'bounce_df', 'bounce_fm', 'bounce_tr'],
list_cleanup: ['bounce_hb', 'spam', 'global_unsubscribe']
};
const eventTypes = smartFilters[filterType];
if (!eventTypes) {
throw new EmailAPIError(`Unknown smart filter type: ${filterType}`, 400);
}
const conditions = eventTypes.map(type => `type==${type}`);
return this.createFilter(conditions, 'or');
}
/**
* Analyze email logs with smart insights
*/
analyzeEmailLogs(logs) {
const data = logs.data || [];
const totalEvents = data.length;
if (totalEvents === 0) {
return {
totalEvents: 0,
eventBreakdown: {},
deliveryRate: 0,
engagementRate: 0,
issueRate: 0,
recommendations: ['No events to analyze']
};
}
// Count events by type
const eventBreakdown = {};
data.forEach(log => {
const type = log.type || 'unknown';
eventBreakdown[type] = (eventBreakdown[type] || 0) + 1;
});
// Calculate rates
const delivered = eventBreakdown.delivered || 0;
const bounced = (eventBreakdown.bounce || 0) + (eventBreakdown.bounce_hb || 0) + (eventBreakdown.bounce_sb || 0);
const opened = eventBreakdown.open || 0;
const clicked = eventBreakdown.click || 0;
const spam = eventBreakdown.spam || 0;
const deliveryRate = delivered > 0 ? (delivered / (delivered + bounced)) * 100 : 0;
const engagementRate = delivered > 0 ? ((opened + clicked) / delivered) * 100 : 0;
const issueRate = totalEvents > 0 ? ((bounced + spam) / totalEvents) * 100 : 0;
// Generate recommendations
const recommendations = [];
if (deliveryRate < 95) {
recommendations.push('Low delivery rate detected. Consider list cleaning and sender reputation monitoring.');
}
if (engagementRate < 15) {
recommendations.push('Low engagement rate. Consider improving subject lines and content quality.');
}
if (issueRate > 5) {
recommendations.push('High issue rate detected. Review bounce handling and spam prevention measures.');
}
if (recommendations.length === 0) {
recommendations.push('Email performance looks healthy. Continue monitoring key metrics.');
}
return {
totalEvents,
eventBreakdown,
deliveryRate: Math.round(deliveryRate * 100) / 100,
engagementRate: Math.round(engagementRate * 100) / 100,
issueRate: Math.round(issueRate * 100) / 100,
recommendations
};
}
/**
* Get email logs with automatic analysis
*/
async getEmailLogsWithAnalysis(options = {}) {
const logs = await this.getEmailLogs(options);
const analysis = this.analyzeEmailLogs(logs);
return { logs, analysis };
}
/**
* Enhanced email validation
*/
isValidEmail(email) {
if (!email || typeof email !== 'string') {
return false;
}
// More comprehensive email validation
const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
return emailRegex.test(email) && email.length <= 254; // RFC 5321 limit
}
}
// Re-export for convenience
export { EmailAPIError } from '../types/errors.js';
//# sourceMappingURL=email-api.js.map