sendgrid.js•20.3 kB
// Enhanced SendGrid Service - Works alongside existing EmailService
import sgMail from '@sendgrid/mail';
export class SendGridService {
pb;
isInitialized = false;
constructor(pb) {
this.pb = pb;
this.initializeSendGrid();
}
initializeSendGrid() {
const apiKey = process.env.SENDGRID_API_KEY;
if (!apiKey) {
console.warn('SendGrid API key not found. SendGrid-specific features will be disabled.');
return;
}
try {
sgMail.setApiKey(apiKey);
this.isInitialized = true;
console.log('SendGrid service initialized successfully');
}
catch (error) {
console.error('Failed to initialize SendGrid:', error);
}
}
// Check if SendGrid is properly initialized
isReady() {
return this.isInitialized;
}
// Enhanced email sending with SendGrid-specific features
async sendEnhancedEmail(data) {
if (!this.isInitialized) {
throw new Error('SendGrid service is not initialized. Check your API key.');
}
try {
const message = {
to: Array.isArray(data.to) ? data.to : [data.to],
from: data.from || process.env.DEFAULT_FROM_EMAIL || process.env.SMTP_USER,
subject: data.subject,
};
// Handle dynamic templates
if (data.templateId) {
message.templateId = data.templateId;
if (data.dynamicTemplateData) {
message.dynamicTemplateData = data.dynamicTemplateData;
}
}
else {
// Regular content
message.html = data.html;
if (data.text) {
message.text = data.text;
}
}
// Add SendGrid-specific options
if (data.options) {
if (data.options.categories) {
message.categories = data.options.categories;
}
if (data.options.customArgs) {
message.customArgs = data.options.customArgs;
}
if (data.options.sendAt) {
message.sendAt = data.options.sendAt;
}
if (data.options.batchId) {
message.batchId = data.options.batchId;
}
if (data.options.asm) {
message.asm = data.options.asm;
}
if (data.options.trackingSettings) {
message.trackingSettings = data.options.trackingSettings;
}
if (data.options.sandboxMode) {
message.mailSettings = {
sandboxMode: {
enable: true
}
};
}
}
// Send email via SendGrid
const response = await sgMail.send(message);
// Log successful email
const emailLog = await this.pb.collection('email_logs').create({
to: Array.isArray(data.to) ? data.to.join(', ') : data.to,
from: message.from,
subject: data.subject,
template: data.templateId || 'custom',
status: 'sent',
variables: data.dynamicTemplateData || {},
sendgrid_message_id: response[0]?.headers['x-message-id'] || null,
categories: data.options?.categories || [],
custom_args: data.options?.customArgs || {}
});
return emailLog;
}
catch (error) {
// Log failed email
const emailLog = await this.pb.collection('email_logs').create({
to: Array.isArray(data.to) ? data.to.join(', ') : data.to,
from: data.from || process.env.DEFAULT_FROM_EMAIL || process.env.SMTP_USER,
subject: data.subject,
template: data.templateId || 'custom',
status: 'failed',
error: error.message,
variables: data.dynamicTemplateData || {}
});
throw new Error(`SendGrid email send failed: ${error.message}`);
}
}
// Create dynamic template in SendGrid
async createDynamicTemplate(data) {
if (!this.isInitialized) {
throw new Error('SendGrid service is not initialized');
}
try {
// Note: This would require SendGrid API template creation
// For now, we'll store the template info in PocketBase and return a placeholder
const template = await this.pb.collection('sendgrid_templates').create({
name: data.name,
subject: data.subject || '',
htmlContent: data.htmlContent || '',
textContent: data.textContent || '',
sendgridTemplateId: `d-${Date.now()}`, // Placeholder ID
active: true
});
return template;
}
catch (error) {
throw new Error(`Failed to create SendGrid template: ${error.message}`);
}
}
// Test SendGrid connection and configuration
async testSendGridConnection() {
if (!this.isInitialized) {
return {
success: false,
message: 'SendGrid API key not configured'
};
}
try {
// Test with a simple validation request
const testMessage = {
to: 'test@example.com',
from: process.env.DEFAULT_FROM_EMAIL || 'test@example.com',
subject: 'Test Connection',
html: '<p>This is a test</p>',
mailSettings: {
sandboxMode: {
enable: true // Sandbox mode - no actual email sent
}
}
};
await sgMail.send(testMessage);
return {
success: true,
message: 'SendGrid connection successful',
features: [
'Dynamic Templates',
'Categories & Tags',
'Custom Arguments',
'Scheduled Sending',
'Click/Open Tracking',
'Unsubscribe Management',
'Sandbox Mode'
]
};
}
catch (error) {
return {
success: false,
message: `SendGrid connection failed: ${error.message}`
};
}
}
// Send bulk emails with batch processing
async sendBulkEmails(emails, options) {
if (!this.isInitialized) {
throw new Error('SendGrid service is not initialized');
}
const results = {
sent: 0,
failed: 0,
errors: []
};
// Process emails in batches to respect rate limits
const batchSize = 100;
for (let i = 0; i < emails.length; i += batchSize) {
const batch = emails.slice(i, i + batchSize);
for (const email of batch) {
try {
await this.sendEnhancedEmail({
...email,
options
});
results.sent++;
}
catch (error) {
results.failed++;
results.errors.push(`${email.to}: ${error.message}`);
}
}
}
return results;
}
// Schedule email sending
async scheduleEmail(data) {
const sendAtTimestamp = Math.floor(data.sendAt.getTime() / 1000);
return this.sendEnhancedEmail({
...data,
options: {
...data.options,
sendAt: sendAtTimestamp
}
});
}
// Cancel scheduled send (requires batch ID)
async cancelScheduledSend(batchId) {
if (!this.isInitialized) {
throw new Error('SendGrid service is not initialized');
}
try {
// Note: This would require SendGrid batch management API
// For now, return success message
return {
success: true,
message: `Scheduled send with batch ID ${batchId} has been cancelled`
};
}
catch (error) {
return {
success: false,
message: `Failed to cancel scheduled send: ${error.message}`
};
}
}
// Advanced SendGrid Features
// Manage email suppressions (unsubscribes, bounces, spam reports)
async getSuppressions(type = 'unsubscribes') {
if (!this.isInitialized) {
throw new Error('SendGrid service is not initialized');
}
try {
// Note: This would use SendGrid Suppression Management API
// For now, return mock data structure
return {
suppressions: [],
count: 0
};
}
catch (error) {
throw new Error(`Failed to retrieve suppressions: ${error.message}`);
}
}
// Add email to suppression list
async addSuppression(email, type = 'unsubscribes') {
if (!this.isInitialized) {
throw new Error('SendGrid service is not initialized');
}
try {
// Note: This would use SendGrid Suppression Management API
// Log the suppression locally
await this.pb.collection('email_suppressions').create({
email,
type,
reason: 'manually_added',
created_at: new Date().toISOString()
});
return {
success: true,
message: `Email ${email} added to ${type} suppression list`
};
}
catch (error) {
return {
success: false,
message: `Failed to add suppression: ${error.message}`
};
}
}
// Remove email from suppression list
async removeSuppression(email, type = 'unsubscribes') {
if (!this.isInitialized) {
throw new Error('SendGrid service is not initialized');
}
try {
// Remove from local database
const suppressions = await this.pb.collection('email_suppressions').getFullList({
filter: `email = "${email}" && type = "${type}"`
});
for (const suppression of suppressions) {
await this.pb.collection('email_suppressions').delete(suppression.id);
}
return {
success: true,
message: `Email ${email} removed from ${type} suppression list`
};
}
catch (error) {
return {
success: false,
message: `Failed to remove suppression: ${error.message}`
};
}
}
// Validate email address using SendGrid
async validateEmail(email) {
if (!this.isInitialized) {
throw new Error('SendGrid service is not initialized');
}
try {
// Note: This would use SendGrid Email Validation API
// For now, provide basic validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const isValid = emailRegex.test(email);
const [local, host] = email.split('@');
return {
valid: isValid,
result: {
email,
verdict: isValid ? 'Valid' : 'Invalid',
score: isValid ? 0.95 : 0.1,
local: local || '',
host: host || '',
suggestion: !isValid ? 'Please check email format' : undefined
}
};
}
catch (error) {
throw new Error(`Email validation failed: ${error.message}`);
}
}
// Get email statistics from SendGrid
async getEmailStats(params) {
if (!this.isInitialized) {
throw new Error('SendGrid service is not initialized');
}
try {
// Note: This would use SendGrid Stats API
// For now, return mock data based on local email logs
const logs = await this.pb.collection('email_logs').getFullList({
filter: `created >= "${params.startDate}"${params.endDate ? ` && created <= "${params.endDate}"` : ''}`,
sort: 'created'
});
// Group by date and calculate stats
const statsMap = new Map();
for (const log of logs) {
const date = log.created.split('T')[0]; // Extract date part
if (!statsMap.has(date)) {
statsMap.set(date, {
date,
delivered: 0,
opens: 0,
clicks: 0,
bounces: 0,
spam_reports: 0,
unsubscribes: 0
});
}
const stats = statsMap.get(date);
if (log.status === 'sent') {
stats.delivered++;
// Mock some engagement metrics
if (Math.random() > 0.7)
stats.opens++;
if (Math.random() > 0.9)
stats.clicks++;
}
else if (log.status === 'failed') {
stats.bounces++;
}
}
return Array.from(statsMap.values());
}
catch (error) {
throw new Error(`Failed to retrieve email stats: ${error.message}`);
}
}
// Create contact list for marketing campaigns
async createContactList(data) {
if (!this.isInitialized) {
throw new Error('SendGrid service is not initialized');
}
try {
// Store contact list locally
const list = await this.pb.collection('sendgrid_contact_lists').create({
name: data.name,
description: data.description || '',
contact_count: data.contacts?.length || 0,
sendgrid_list_id: `list_${Date.now()}`
});
// Store contacts if provided
if (data.contacts) {
for (const contact of data.contacts) {
await this.pb.collection('sendgrid_contacts').create({
list_id: list.id,
email: contact.email,
first_name: contact.firstName || '',
last_name: contact.lastName || '',
custom_fields: contact.customFields || {}
});
}
}
return {
id: list.id,
name: list.name,
contactCount: data.contacts?.length || 0,
created: list.created
};
}
catch (error) {
throw new Error(`Failed to create contact list: ${error.message}`);
}
}
// Add contact to existing list
async addContactToList(listId, contact) {
if (!this.isInitialized) {
throw new Error('SendGrid service is not initialized');
}
try {
// Check if list exists
const list = await this.pb.collection('sendgrid_contact_lists').getOne(listId);
// Add contact
await this.pb.collection('sendgrid_contacts').create({
list_id: listId,
email: contact.email,
first_name: contact.firstName || '',
last_name: contact.lastName || '',
custom_fields: contact.customFields || {}
});
// Update contact count
const currentCount = await this.pb.collection('sendgrid_contacts').getFullList({
filter: `list_id = "${listId}"`
});
await this.pb.collection('sendgrid_contact_lists').update(listId, {
contact_count: currentCount.length
});
return {
success: true,
message: `Contact ${contact.email} added to list ${list.name}`
};
}
catch (error) {
return {
success: false,
message: `Failed to add contact to list: ${error.message}`
};
}
}
// Get webhook event data processing
async processWebhookEvent(eventData) {
try {
// Log the webhook event
await this.pb.collection('sendgrid_webhook_events').create({
email: eventData.email,
event: eventData.event,
timestamp: new Date(eventData.timestamp * 1000).toISOString(),
sg_message_id: eventData.sg_message_id || '',
useragent: eventData.useragent || '',
ip: eventData.ip || '',
url: eventData.url || '',
reason: eventData.reason || ''
}); // Update email log status if we can find the matching log
if (eventData.sg_message_id) {
try {
const emailLogs = await this.pb.collection('email_logs').getFullList({
filter: `sendgrid_message_id = "${eventData.sg_message_id}"`
});
for (const log of emailLogs) {
// Update status based on event
let newStatus = log.status;
if (eventData.event === 'delivered')
newStatus = 'sent';
if (eventData.event === 'bounce' || eventData.event === 'dropped')
newStatus = 'failed';
await this.pb.collection('email_logs').update(log.id, {
status: newStatus,
last_event: eventData.event,
last_event_timestamp: new Date(eventData.timestamp * 1000).toISOString()
});
}
}
catch (error) {
// Continue even if we can't update email logs
console.warn('Could not update email log for webhook event:', error);
}
}
// Handle suppressions automatically
if (eventData.event === 'bounce' || eventData.event === 'spamreport' || eventData.event === 'unsubscribe') {
await this.addSuppression(eventData.email, eventData.event === 'unsubscribe' ? 'unsubscribes' :
eventData.event === 'spamreport' ? 'spam_reports' : 'bounces');
}
return {
success: true,
message: `Webhook event ${eventData.event} processed for ${eventData.email}`
};
}
catch (error) {
return {
success: false,
message: `Failed to process webhook event: ${error.message}`
};
}
}
// Register SendGrid-related tools dynamically
static registerTools(server, pb) {
server.tool('sendgrid_email', 'Send an email via SendGrid', { type: 'object', properties: { to: { type: 'string' }, subject: { type: 'string' }, body: { type: 'string' } } }, async (args) => {
const sendGridService = new SendGridService(pb);
await sendGridService.sendEmail(args.to, args.subject, args.body);
return { success: true };
});
}
async sendEmail(to, subject, body) {
sgMail.setApiKey(process.env.SENDGRID_API_KEY || '');
const msg = {
to,
from: process.env.EMAIL_FROM || '',
subject,
text: body,
};
await sgMail.send(msg);
}
}