UNPKG

mailblock

Version:

Official Node.js SDK for Mailblock - send, schedule, cancel and update emails with ease

930 lines (826 loc) 27 kB
class EmailBuilder { constructor(client) { this.client = client; this.emailData = {}; } to(emails) { this.emailData.to = this._validateEmailOrArray(emails, 'to'); return this; } cc(emails) { if (emails !== undefined) { this.emailData.cc = this._validateEmailOrArray(emails, 'cc'); } return this; } bcc(emails) { if (emails !== undefined) { this.emailData.bcc = this._validateEmailOrArray(emails, 'bcc'); } return this; } from(email) { if (!this._isValidEmail(email)) { throw new Error(`Invalid 'from' email address: ${email}`); } this.emailData.from = email; return this; } subject(subject) { if ( !subject || typeof subject !== "string" || subject.trim().length === 0 ) { throw new Error("Subject must be a non-empty string"); } this.emailData.subject = subject.trim(); return this; } text(content) { if (!content || typeof content !== "string") { throw new Error("Text content must be a non-empty string"); } this.emailData.text = content; return this; } html(content) { if (!content || typeof content !== "string") { throw new Error("HTML content must be a non-empty string"); } this.emailData.html = content; return this; } scheduleAt(date) { if (date instanceof Date) { if (date <= new Date()) { throw new Error("Scheduled date must be in the future"); } this.emailData.scheduledAt = date; } else if (typeof date === "string") { const parsedDate = new Date(date); if (isNaN(parsedDate.getTime())) { throw new Error("Invalid date format for scheduling"); } if (parsedDate <= new Date()) { throw new Error("Scheduled date must be in the future"); } this.emailData.scheduledAt = parsedDate; } else { throw new Error( "Scheduled date must be a Date object or valid date string" ); } return this; } async send() { return this.client.sendEmail(this.emailData); } _isValidEmail(email) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); } _validateEmailOrArray(emails, fieldName) { if (typeof emails === 'string') { if (!this._isValidEmail(emails)) { throw new Error(`Invalid '${fieldName}' email address: ${emails}`); } return emails; } if (Array.isArray(emails)) { if (emails.length === 0) { throw new Error(`${fieldName} array cannot be empty`); } for (const email of emails) { if (typeof email !== 'string' || !this._isValidEmail(email)) { throw new Error(`Invalid '${fieldName}' email address: ${email}`); } } return emails; } throw new Error(`${fieldName} must be a string or array of strings`); } } class Mailblock { constructor(apiKey, options = {}) { if (!apiKey) { throw new Error("API key is required"); } if (typeof apiKey !== "string" || apiKey.trim().length === 0) { throw new Error("API key must be a non-empty string"); } this.apiKey = apiKey.trim(); this.baseUrl = "https://sdk-backend-production-20e1.up.railway.app"; this.debug = options.debug || false; this.logger = options.logger || console; } _log(level, message, data = null) { if (!this.debug) return; const timestamp = new Date().toISOString(); const logMessage = `[${timestamp}] Mailblock ${level.toUpperCase()}: ${message}`; if (data) { this.logger[level](logMessage, data); } else { this.logger[level](logMessage); } } _generateRequestId() { return 'req_' + Math.random().toString(36).substring(2, 11) + Date.now().toString(36); } email() { return new EmailBuilder(this); } async sendEmail({ to, cc, bcc, from, subject, text, html, scheduledAt }) { const requestId = this._generateRequestId(); const startTime = Date.now(); const timestamp = new Date().toISOString(); this._log('info', `Initiating email send request`, { requestId, to, from, subject: subject?.substring(0, 50) + '...' }); // Validation errors if (!to) { return { success: false, error: "Recipient email address (to) is required", errorType: "VALIDATION_ERROR", statusCode: null, requestId, timestamp, duration: Date.now() - startTime }; } if (!from) { return { success: false, error: "Sender email address (from) is required", errorType: "VALIDATION_ERROR", statusCode: null, requestId, timestamp, duration: Date.now() - startTime }; } if (!subject) { return { success: false, error: "Email subject is required", errorType: "VALIDATION_ERROR", statusCode: null, requestId, timestamp, duration: Date.now() - startTime }; } if (!text && !html) { return { success: false, error: "Either text or html content is required", errorType: "VALIDATION_ERROR", statusCode: null, requestId, timestamp, duration: Date.now() - startTime }; } // Validate email addresses const emailValidation = this._validateEmailFields({ to, cc, bcc, from }); if (!emailValidation.isValid) { return { success: false, error: emailValidation.error, errorType: "VALIDATION_ERROR", statusCode: null, requestId, timestamp, duration: Date.now() - startTime }; } const payload = { to, from, subject, ...(text && { text }), ...(html && { html }), ...(cc && { cc }), ...(bcc && { bcc }), }; if (scheduledAt) { payload.scheduled_at = scheduledAt instanceof Date ? scheduledAt.toISOString() : scheduledAt; } this._log('debug', 'Sending API request', { requestId, endpoint: `${this.baseUrl}/v1/send-email`, payload: { ...payload, text: text ? '[REDACTED]' : undefined, html: html ? '[REDACTED]' : undefined } }); try { const response = await fetch(`${this.baseUrl}/v1/send-email`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.apiKey}`, "X-Request-ID": requestId, }, body: JSON.stringify(payload), }); const result = await response.json(); const duration = Date.now() - startTime; this._log('debug', `API response received`, { requestId, statusCode: response.status, duration: `${duration}ms`, success: response.ok }); if (!response.ok) { const errorType = this._categorizeError(response.status); const errorMessage = result.error || `HTTP error! status: ${response.status}`; const suggestion = this._getErrorSuggestion(response.status); this._log('error', `API request failed`, { requestId, error: errorMessage, statusCode: response.status, errorType, suggestion }); return { success: false, error: errorMessage, errorType, suggestion, statusCode: response.status, requestId, timestamp, duration, endpoint: `${this.baseUrl}/v1/send-email` }; } // Handle the new API response format const emailData = result.results && result.results[0] ? result.results[0] : result; this._log('info', `Email ${scheduledAt ? 'scheduled' : 'sent'} successfully`, { requestId, duration: `${duration}ms`, emailId: emailData.id, successCount: result.success_count, totalRecipients: result.total_recipients }); return { success: true, data: { id: emailData.id, status: emailData.status, to: emailData.to, cc: emailData.cc, bcc: emailData.bcc, success_count: result.success_count, error_count: result.error_count, total_recipients: result.total_recipients, usage: result.usage }, message: scheduledAt ? "Email scheduled successfully" : "Email sent successfully", requestId, timestamp, duration }; } catch (error) { const duration = Date.now() - startTime; const errorType = error.name === 'TypeError' && error.message.includes('fetch') ? 'NETWORK_ERROR' : 'UNKNOWN_ERROR'; this._log('error', `Request failed with exception`, { requestId, error: error.message, errorType, duration: `${duration}ms` }); return { success: false, error: `Failed to send email: ${error.message}`, errorType, suggestion: errorType === 'NETWORK_ERROR' ? 'Check your internet connection and try again' : 'Please try again or contact support if the issue persists', statusCode: null, requestId, timestamp, duration, endpoint: `${this.baseUrl}/v1/send-email` }; } } async cancelEmail(emailId) { const requestId = this._generateRequestId(); const startTime = Date.now(); const timestamp = new Date().toISOString(); this._log('info', `Initiating email cancellation request`, { requestId, emailId }); // Validation if (!emailId) { return { success: false, error: "Email ID is required", errorType: "VALIDATION_ERROR", statusCode: null, requestId, timestamp, duration: Date.now() - startTime }; } if (typeof emailId !== 'number' && typeof emailId !== 'string') { return { success: false, error: "Email ID must be a number or string", errorType: "VALIDATION_ERROR", statusCode: null, requestId, timestamp, duration: Date.now() - startTime }; } this._log('debug', 'Sending cancellation API request', { requestId, endpoint: `${this.baseUrl}/v1/cancel-email/${emailId}`, emailId }); try { const response = await fetch(`${this.baseUrl}/v1/cancel-email/${emailId}`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.apiKey}`, "X-Request-ID": requestId, }, }); const result = await response.json(); const duration = Date.now() - startTime; this._log('debug', `Cancellation API response received`, { requestId, statusCode: response.status, duration: `${duration}ms`, success: response.ok }); if (!response.ok) { const errorType = this._categorizeError(response.status); const errorMessage = result.error || `HTTP error! status: ${response.status}`; const suggestion = this._getErrorSuggestion(response.status); this._log('error', `Cancellation API request failed`, { requestId, error: errorMessage, statusCode: response.status, errorType, suggestion }); return { success: false, error: errorMessage, errorType, suggestion, statusCode: response.status, requestId, timestamp, duration, endpoint: `${this.baseUrl}/v1/cancel-email/${emailId}` }; } this._log('info', `Email cancelled successfully`, { requestId, duration: `${duration}ms`, emailId: result.email_id, previousStatus: result.previous_status, currentStatus: result.current_status }); return { success: true, data: result, message: "Email cancelled successfully", requestId, timestamp, duration }; } catch (error) { const duration = Date.now() - startTime; const errorType = error.name === 'TypeError' && error.message.includes('fetch') ? 'NETWORK_ERROR' : 'UNKNOWN_ERROR'; this._log('error', `Cancellation request failed with exception`, { requestId, error: error.message, errorType, duration: `${duration}ms` }); return { success: false, error: `Failed to cancel email: ${error.message}`, errorType, suggestion: errorType === 'NETWORK_ERROR' ? 'Check your internet connection and try again' : 'Please try again or contact support if the issue persists', statusCode: null, requestId, timestamp, duration, endpoint: `${this.baseUrl}/v1/cancel-email/${emailId}` }; } } async cancelEmails(emailIds) { const requestId = this._generateRequestId(); const startTime = Date.now(); const timestamp = new Date().toISOString(); this._log('info', `Initiating bulk email cancellation request`, { requestId, emailIds, count: emailIds?.length }); // Validation if (!emailIds) { return { success: false, error: "Email IDs array is required", errorType: "VALIDATION_ERROR", statusCode: null, requestId, timestamp, duration: Date.now() - startTime }; } if (!Array.isArray(emailIds)) { return { success: false, error: "Email IDs must be an array", errorType: "VALIDATION_ERROR", statusCode: null, requestId, timestamp, duration: Date.now() - startTime }; } if (emailIds.length === 0) { return { success: false, error: "Email IDs array cannot be empty", errorType: "VALIDATION_ERROR", statusCode: null, requestId, timestamp, duration: Date.now() - startTime }; } // Validate each email ID for (const emailId of emailIds) { if (typeof emailId !== 'number' && typeof emailId !== 'string') { return { success: false, error: `Invalid email ID: ${emailId}. All email IDs must be numbers or strings`, errorType: "VALIDATION_ERROR", statusCode: null, requestId, timestamp, duration: Date.now() - startTime }; } } const payload = { email_ids: emailIds }; this._log('debug', 'Sending bulk cancellation API request', { requestId, endpoint: `${this.baseUrl}/v1/cancel-email`, payload }); try { const response = await fetch(`${this.baseUrl}/v1/cancel-email`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.apiKey}`, "X-Request-ID": requestId, }, body: JSON.stringify(payload), }); const result = await response.json(); const duration = Date.now() - startTime; this._log('debug', `Bulk cancellation API response received`, { requestId, statusCode: response.status, duration: `${duration}ms`, success: response.ok, successCount: result.success_count, errorCount: result.error_count }); if (!response.ok) { const errorType = this._categorizeError(response.status); const errorMessage = result.error || `HTTP error! status: ${response.status}`; const suggestion = this._getErrorSuggestion(response.status); this._log('error', `Bulk cancellation API request failed`, { requestId, error: errorMessage, statusCode: response.status, errorType, suggestion }); return { success: false, error: errorMessage, errorType, suggestion, statusCode: response.status, requestId, timestamp, duration, endpoint: `${this.baseUrl}/v1/cancel-email` }; } this._log('info', `Bulk email cancellation completed`, { requestId, duration: `${duration}ms`, successCount: result.success_count, errorCount: result.error_count, totalRequested: emailIds.length }); return { success: true, data: result, message: result.message || `Cancelled ${result.success_count} of ${emailIds.length} emails`, requestId, timestamp, duration }; } catch (error) { const duration = Date.now() - startTime; const errorType = error.name === 'TypeError' && error.message.includes('fetch') ? 'NETWORK_ERROR' : 'UNKNOWN_ERROR'; this._log('error', `Bulk cancellation request failed with exception`, { requestId, error: error.message, errorType, duration: `${duration}ms` }); return { success: false, error: `Failed to cancel emails: ${error.message}`, errorType, suggestion: errorType === 'NETWORK_ERROR' ? 'Check your internet connection and try again' : 'Please try again or contact support if the issue persists', statusCode: null, requestId, timestamp, duration, endpoint: `${this.baseUrl}/v1/cancel-email` }; } } async updateScheduledEmail(emailId, updates) { const requestId = this._generateRequestId(); const startTime = Date.now(); const timestamp = new Date().toISOString(); this._log('info', `Initiating scheduled email update request`, { requestId, emailId, updates: Object.keys(updates) }); // Validation if (!emailId) { return { success: false, error: "Email ID is required", errorType: "VALIDATION_ERROR", statusCode: null, requestId, timestamp, duration: Date.now() - startTime }; } if (typeof emailId !== 'number' && typeof emailId !== 'string') { return { success: false, error: "Email ID must be a number or string", errorType: "VALIDATION_ERROR", statusCode: null, requestId, timestamp, duration: Date.now() - startTime }; } if (!updates || typeof updates !== 'object') { return { success: false, error: "Updates object is required", errorType: "VALIDATION_ERROR", statusCode: null, requestId, timestamp, duration: Date.now() - startTime }; } // Validate that at least one valid field is provided const validFields = ['subject', 'body_html', 'body_text', 'scheduled_at']; const providedFields = Object.keys(updates).filter(key => validFields.includes(key)); if (providedFields.length === 0) { return { success: false, error: "At least one field must be provided for update (subject, body_html, body_text, or scheduled_at)", errorType: "VALIDATION_ERROR", statusCode: null, requestId, timestamp, duration: Date.now() - startTime }; } // Prepare payload - only include valid fields const payload = {}; if (updates.subject !== undefined) payload.subject = updates.subject; if (updates.body_html !== undefined) payload.body_html = updates.body_html; if (updates.body_text !== undefined) payload.body_text = updates.body_text; if (updates.scheduled_at !== undefined) { // Handle Date objects or strings if (updates.scheduled_at instanceof Date) { payload.scheduled_at = updates.scheduled_at.toISOString(); } else if (updates.scheduled_at === null) { payload.scheduled_at = null; } else if (typeof updates.scheduled_at === 'string') { // Validate date string const parsedDate = new Date(updates.scheduled_at); if (isNaN(parsedDate.getTime())) { return { success: false, error: "Invalid scheduled_at date format", errorType: "VALIDATION_ERROR", statusCode: null, requestId, timestamp, duration: Date.now() - startTime }; } payload.scheduled_at = updates.scheduled_at; } else { return { success: false, error: "scheduled_at must be a Date object, valid date string, or null", errorType: "VALIDATION_ERROR", statusCode: null, requestId, timestamp, duration: Date.now() - startTime }; } } this._log('debug', 'Sending update API request', { requestId, endpoint: `${this.baseUrl}/v1/update-scheduled-email/${emailId}`, payload: { ...payload, body_html: payload.body_html ? '[REDACTED]' : undefined, body_text: payload.body_text ? '[REDACTED]' : undefined } }); try { const response = await fetch(`${this.baseUrl}/v1/update-scheduled-email/${emailId}`, { method: "PUT", headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.apiKey}`, "X-Request-ID": requestId, }, body: JSON.stringify(payload), }); const result = await response.json(); const duration = Date.now() - startTime; this._log('debug', `Update API response received`, { requestId, statusCode: response.status, duration: `${duration}ms`, success: response.ok }); if (!response.ok) { const errorType = this._categorizeError(response.status); const errorMessage = result.error || `HTTP error! status: ${response.status}`; const suggestion = this._getErrorSuggestion(response.status); this._log('error', `Update API request failed`, { requestId, error: errorMessage, statusCode: response.status, errorType, suggestion, currentStatus: result.current_status }); return { success: false, error: errorMessage, errorType, suggestion, statusCode: response.status, currentStatus: result.current_status, requestId, timestamp, duration, endpoint: `${this.baseUrl}/v1/update-scheduled-email/${emailId}` }; } this._log('info', `Scheduled email updated successfully`, { requestId, duration: `${duration}ms`, emailId: result.email?.id, status: result.email?.status, trackingUpdated: result.tracking_updated, jobRescheduled: result.job_rescheduled }); return { success: true, data: { message: result.message, email: result.email, tracking_updated: result.tracking_updated, job_rescheduled: result.job_rescheduled }, message: "Email updated successfully", requestId, timestamp, duration }; } catch (error) { const duration = Date.now() - startTime; const errorType = error.name === 'TypeError' && error.message.includes('fetch') ? 'NETWORK_ERROR' : 'UNKNOWN_ERROR'; this._log('error', `Update request failed with exception`, { requestId, error: error.message, errorType, duration: `${duration}ms` }); return { success: false, error: `Failed to update scheduled email: ${error.message}`, errorType, suggestion: errorType === 'NETWORK_ERROR' ? 'Check your internet connection and try again' : 'Please try again or contact support if the issue persists', statusCode: null, requestId, timestamp, duration, endpoint: `${this.baseUrl}/v1/update-scheduled-email/${emailId}` }; } } _categorizeError(statusCode) { if (statusCode >= 400 && statusCode < 500) { return 'CLIENT_ERROR'; } else if (statusCode >= 500) { return 'SERVER_ERROR'; } else if (statusCode === 429) { return 'RATE_LIMIT_ERROR'; } return 'UNKNOWN_ERROR'; } _getErrorSuggestion(statusCode) { switch (statusCode) { case 400: return 'Check your request parameters and try again'; case 401: return 'Verify your API key is correct and has proper permissions'; case 403: return 'Your API key may not have permission for this operation'; case 404: return 'The API endpoint was not found. Check the base URL'; case 429: return 'You are being rate limited. Wait a moment and try again'; case 500: return 'Server error occurred. Try again in a few moments'; case 503: return 'Service temporarily unavailable. Please try again later'; default: return 'Please try again or contact support if the issue persists'; } } _validateEmailFields({ to, cc, bcc, from }) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; // Helper to validate single email or array const validateEmailOrArray = (emails, fieldName) => { if (typeof emails === 'string') { if (!emailRegex.test(emails)) { return { isValid: false, error: `Invalid ${fieldName} email address: ${emails}` }; } } else if (Array.isArray(emails)) { if (emails.length === 0) { return { isValid: false, error: `${fieldName} array cannot be empty` }; } for (const email of emails) { if (typeof email !== 'string' || !emailRegex.test(email)) { return { isValid: false, error: `Invalid ${fieldName} email address: ${email}` }; } } } else { return { isValid: false, error: `${fieldName} must be a string or array of strings` }; } return { isValid: true }; }; // Validate 'to' field const toValidation = validateEmailOrArray(to, 'recipient'); if (!toValidation.isValid) return toValidation; // Validate 'from' field (always single email) if (!emailRegex.test(from)) { return { isValid: false, error: `Invalid sender email address: ${from}` }; } // Validate 'cc' field if provided if (cc !== undefined) { const ccValidation = validateEmailOrArray(cc, 'cc'); if (!ccValidation.isValid) return ccValidation; } // Validate 'bcc' field if provided if (bcc !== undefined) { const bccValidation = validateEmailOrArray(bcc, 'bcc'); if (!bccValidation.isValid) return bccValidation; } return { isValid: true }; } } export default Mailblock; export { EmailBuilder };