payload-email-ahasend
Version:
Payload Ahasend Email Adapter
284 lines (283 loc) • 10.6 kB
JavaScript
import { APIError } from 'payload';
/**
* Email Parser utility for handling various email formats
* @class EmailParser
*/ class EmailParser {
/**
* Parses an email string in format "Name <email@example.com>" or just "email@example.com"
* @static
* @param {string} emailStr - The email string to parse
* @returns {EmailRecipient} - Parsed email recipient object
*/ static parseEmailString(emailStr) {
// Optimize the regex to be more precise and handle more cases
const match = emailStr.match(/^(?:([^<]+)<([^>]+)>|([^<\s]+))$/);
if (match) {
const [, name, emailInBrackets, emailWithoutBrackets] = match;
if (emailInBrackets) {
return {
name: name?.trim(),
email: emailInBrackets.trim()
};
}
return {
email: emailWithoutBrackets?.trim() || emailStr.trim()
};
}
// Fallback to ensure we always return a valid structure
return {
email: emailStr.trim()
};
}
}
/**
* Email Builder using the builder pattern
* @class AhasendEmailBuilder
*/ class AhasendEmailBuilder {
email;
/**
* Creates an instance of AhasendEmailBuilder
* @param {AhasendAdapterConfig} config - Configuration for the email defaults
*/ constructor(config){
this.email = {
content: {
subject: ''
},
from: {
name: config.defaultFromName,
email: config.defaultFromAddress
},
recipients: []
};
}
/**
* Builds and validates the final email object
* @returns {AhasendEmail} - The complete email object ready to send
* @throws {APIError} - If no recipients are defined
*/ build() {
if (!this.email.recipients.length) {
throw new APIError('Email must have at least one recipient', 400);
}
return this.email;
}
/**
* Sets the attachments for the email
* @param {SendEmailOptions["attachments"]} attachments - Array of attachments
* @returns {this} - Returns this instance for method chaining
* @throws {APIError} - If attachment is missing required properties
*/ setAttachments(attachments) {
if (!attachments?.length) {
return this;
}
this.email.content.attachments = attachments.map((attachment)=>{
if (!attachment.filename || !attachment.content) {
throw new APIError('Attachment is missing filename or content', 400);
}
let data;
// Optimize data handling with clearer logic
if (Buffer.isBuffer(attachment.content)) {
data = attachment.content.toString('base64');
} else if (typeof attachment.content === 'string') {
// Check if it's already a base64 data URI
if (attachment.content.startsWith('data:')) {
const parts = attachment.content.split(',');
data = parts[1] || '';
} else {
// Convert string to base64
data = Buffer.from(attachment.content).toString('base64');
}
} else {
throw new APIError('Attachment content must be a string or Buffer', 400);
}
return {
base64: true,
content_type: attachment.contentType || 'application/octet-stream',
data,
file_name: attachment.filename
};
});
return this;
}
/**
* Sets the email content (subject, HTML, and plain text)
* @param {SendEmailOptions} message - Email content options
* @returns {this} - Returns this instance for method chaining
*/ setContent(message) {
this.email.content.subject = message.subject || '';
// Only set body properties if they exist
if (message.html) {
this.email.content.html_body = String(message.html);
}
if (message.text) {
this.email.content.text_body = String(message.text);
}
return this;
}
/**
* Sets the from field for the email
* @param {SendEmailOptions["from"]} from - From address
* @param {string} defaultFromAddress - Default sender address
* @param {string} defaultFromName - Default sender name
* @returns {this} - Returns this instance for method chaining
*/ setFrom(from, defaultFromAddress, defaultFromName) {
if (!from) {
return this;
}
if (typeof from === 'string') {
const parsed = EmailParser.parseEmailString(from);
this.email.from = {
name: parsed.name || defaultFromName,
email: parsed.email || defaultFromAddress
};
} else {
this.email.from = {
name: from.name || defaultFromName,
email: from.address || defaultFromAddress
};
}
return this;
}
/**
* Sets the recipients for the email
* @param {SendEmailOptions["to"]} addresses - Recipient addresses
* @returns {this} - Returns this instance for method chaining
*/ setRecipients(addresses) {
if (!addresses) {
return this;
}
const recipients = [];
if (typeof addresses === 'string') {
recipients.push(EmailParser.parseEmailString(addresses));
} else if (Array.isArray(addresses)) {
addresses.forEach((address)=>{
if (typeof address === 'string') {
recipients.push(EmailParser.parseEmailString(address));
} else if (address?.address) {
recipients.push({
name: address.name,
email: address.address
});
}
});
} else if (addresses?.address) {
recipients.push({
name: addresses.name,
email: addresses.address
});
}
this.email.recipients = recipients;
return this;
}
/**
* Sets the reply-to field for the email
* @param {SendEmailOptions["replyTo"]} replyTo - Reply-to address
* @returns {this} - Returns this instance for method chaining
*/ setReplyTo(replyTo) {
if (!replyTo) {
return this;
}
if (typeof replyTo === 'string') {
this.email.content.reply_to = EmailParser.parseEmailString(replyTo);
} else if (Array.isArray(replyTo) && replyTo.length > 0) {
const first = replyTo[0];
if (typeof first === 'string') {
this.email.content.reply_to = EmailParser.parseEmailString(first);
} else if (first?.address) {
this.email.content.reply_to = {
name: first.name,
email: first.address
};
}
} else if (!Array.isArray(replyTo) && replyTo?.address) {
this.email.content.reply_to = {
name: replyTo.name,
email: replyTo.address
};
}
return this;
}
}
/**
* Service class for sending emails via Ahasend API
* @class AhasendEmailService
*/ class AhasendEmailService {
apiKey;
/**
* Creates an instance of AhasendEmailService
* @param {string} apiKey - API key for Ahasend service
*/ constructor(apiKey){
this.apiKey = apiKey;
}
/**
* Sends an email using the Ahasend API
* @param {AhasendEmail} emailOptions - Email to send
* @returns {Promise<AhasendResponse>} - API response
* @throws {APIError} - If the API request fails
*/ async sendEmail(emailOptions) {
try {
const response = await fetch('https://api.ahasend.com/v1/email/send', {
body: JSON.stringify(emailOptions),
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'X-Api-Key': this.apiKey
},
method: 'POST'
});
if (!response.ok) {
throw new APIError(`API request failed with status ${response.status}`, response.status);
}
const data = await response.json();
if ('success_count' in data && 'fail_count' in data && data.fail_count === 0) {
return data;
} else {
const errorMessage = 'status' in data ? data.status : 'Unknown error';
throw new APIError(errorMessage, response.status);
}
} catch (error) {
if (error instanceof APIError) {
throw error;
}
throw new APIError(`Failed to send email: ${error.message}`, 500);
}
}
}
/**
* Creates an Ahasend email adapter for Payload CMS
* @function ahasendAdapter
* @param {AhasendAdapterConfig} config - Configuration for the adapter
* @returns {EmailAdapter<AhasendResponse>} - Configured email adapter
* @example
* // Create and configure the adapter
* const emailAdapter = ahasendAdapter({
* apiKey: 'your-ahasend-api-key',
* defaultFromAddress: 'noreply@example.com',
* defaultFromName: 'Example Company'
* });
*
* // Use in Payload config
* export const config = {
* email: emailAdapter(),
* // rest of Payload config
* };
*/ export const ahasendAdapter = (config)=>{
const { apiKey, defaultFromAddress, defaultFromName } = config;
const emailService = new AhasendEmailService(apiKey);
return ()=>({
name: 'ahasend-rest',
defaultFromAddress,
defaultFromName,
sendEmail: async (message)=>{
try {
const emailBuilder = new AhasendEmailBuilder(config).setFrom(message.from, defaultFromAddress, defaultFromName).setRecipients(message.to).setReplyTo(message.replyTo).setContent(message).setAttachments(message.attachments);
const emailOptions = emailBuilder.build();
return await emailService.sendEmail(emailOptions);
} catch (error) {
if (error instanceof APIError) {
throw error;
}
throw new APIError(`Failed to send email: ${error.message}`, 500);
}
}
});
};
//# sourceMappingURL=index.js.map