mailgun-optin-cli
Version:
CLI tool for sending opt-in confirmation emails via Mailgun
251 lines (213 loc) • 6.93 kB
JavaScript
import Mailgun from 'mailgun.js';
import FormData from 'form-data';
import { validateEmail } from './csv-parser.js';
/**
* Validates Mailgun configuration
* @param {Object} config - Mailgun configuration object
* @throws {Error} - If configuration is invalid
*/
export const validateMailgunConfig = config => {
if (!config.apiKey) {
throw new Error('Mailgun API key is required');
}
if (!config.domain) {
throw new Error('Mailgun domain is required');
}
if (!config.fromEmail) {
throw new Error('From email address is required');
}
if (!validateEmail(config.fromEmail)) {
throw new Error('From email address is invalid');
}
};
/**
* Creates a Mailgun client with validated configuration
* @param {Object} config - Mailgun configuration
* @returns {Object} - Mailgun client instance
*/
export const createMailgunClient = config => {
// Set default fromName if not provided
const normalizedConfig = {
...config,
fromName: config.fromName || 'Mailer',
};
validateMailgunConfig(normalizedConfig);
const mailgun = new Mailgun(FormData);
const mg = mailgun.client({
username: 'api',
key: normalizedConfig.apiKey,
});
return {
config: normalizedConfig,
mailgun: mg,
};
};
/**
* Validates email data before sending
* @param {Object} emailData - Email data to validate
* @throws {Error} - If email data is invalid
*/
const validateEmailData = emailData => {
if (!emailData.to) {
throw new Error('Recipient email is required');
}
if (!validateEmail(emailData.to)) {
throw new Error('Invalid recipient email format');
}
if (!emailData.subject) {
throw new Error('Email subject is required');
}
if (!emailData.text && !emailData.html) {
throw new Error('Either text or html content is required');
}
};
/**
* Sends a single email via Mailgun
* @param {Object} client - Mailgun client instance
* @param {Object} emailData - Email data
* @returns {Promise<Object>} - Send result
*/
export const sendEmail = async (client, emailData) => {
try {
validateEmailData(emailData);
// Extract local part from FROM_EMAIL for the sender address
const fromEmailParts = client.config.fromEmail.split('@');
const localPart = fromEmailParts[0];
// Use Mailgun domain for the From address to avoid rewriting
const fromAddress = `${localPart}@${client.config.domain}`;
const messageData = {
from: `${client.config.fromName} <${fromAddress}>`,
to: emailData.to,
subject: emailData.subject,
text: emailData.text,
html: emailData.html,
};
// Set Reply-To to the original FROM_EMAIL so replies go to the right place
messageData['h:Reply-To'] = `${client.config.fromName} <${client.config.fromEmail}>`;
// Add custom headers if provided
if (emailData.headers) {
Object.entries(emailData.headers).forEach(([key, value]) => {
messageData[`h:${key}`] = value;
});
}
const response = await client.mailgun.messages.create(client.config.domain, messageData);
return {
success: true,
messageId: response.id,
response: response.message,
};
} catch (error) {
return {
success: false,
error: error.message,
};
}
};
/**
* Processes template placeholders in text
* @param {string} template - Template string with placeholders
* @param {Object} data - Data to replace placeholders
* @returns {string} - Processed template
*/
const processTemplate = (template, data) => {
if (!template) return template;
return template.replace(/\{(\w+)\}/g, (match, key) => {
return data[key] || '';
});
};
/**
* Creates a delay for rate limiting
* @param {number} ms - Milliseconds to delay
* @returns {Promise} - Promise that resolves after delay
*/
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
/**
* Sends bulk emails to multiple recipients with rate limiting
* @param {Object} client - Mailgun client instance
* @param {Array} subscribers - Array of subscriber objects
* @param {Object} emailTemplate - Email template with placeholders
* @param {Object} options - Options for bulk sending
* @returns {Promise<Array>} - Array of send results
*/
export const sendBulkEmails = async (client, subscribers, emailTemplate, options = {}) => {
const { rateLimit = 10, onProgress } = options; // Default 10 emails per second
const delayBetweenEmails = 1000 / rateLimit;
const results = [];
const total = subscribers.length;
for (let i = 0; i < subscribers.length; i++) {
const subscriber = subscribers[i];
// Process template placeholders
const processedEmail = {
to: subscriber.email,
subject: processTemplate(emailTemplate.subject, subscriber),
text: processTemplate(emailTemplate.text, subscriber),
html: processTemplate(emailTemplate.html, subscriber),
};
// Add any additional headers from template
if (emailTemplate.headers) {
processedEmail.headers = emailTemplate.headers;
}
const result = await sendEmail(client, processedEmail);
const emailResult = {
email: subscriber.email,
...result,
};
results.push(emailResult);
// Call progress callback if provided
if (onProgress) {
onProgress({
current: i + 1,
total,
result: emailResult,
percentage: Math.round(((i + 1) / total) * 100),
});
}
// Rate limiting: delay between emails (except for the last one)
if (i < subscribers.length - 1) {
await delay(delayBetweenEmails);
}
}
return results;
};
/**
* Gets statistics from bulk email results
* @param {Array} results - Array of send results
* @returns {Object} - Statistics object
*/
export const getBulkEmailStats = results => {
const total = results.length;
const successful = results.filter(r => r.success).length;
const failed = results.filter(r => !r.success).length;
return {
total,
successful,
failed,
successRate: total > 0 ? (successful / total) * 100 : 0,
};
};
/**
* Validates Mailgun connection by sending a test email
* @param {Object} client - Mailgun client instance
* @param {string} testEmail - Email address to send test to
* @returns {Promise<Object>} - Validation result
*/
export const validateMailgunConnection = async (client, testEmail) => {
const testEmailData = {
to: testEmail,
subject: 'Mailgun Connection Test',
text: 'This is a test email to validate your Mailgun configuration.',
html: '<p>This is a test email to validate your Mailgun configuration.</p>',
};
try {
const result = await sendEmail(client, testEmailData);
return {
valid: result.success,
message: result.success ? 'Connection successful' : result.error,
};
} catch (error) {
return {
valid: false,
message: `Connection failed: ${error.message}`,
};
}
};