@smartsamurai/krapi-sdk
Version:
KRAPI TypeScript SDK - Easy-to-use client SDK for connecting to self-hosted KRAPI servers (like Appwrite SDK)
1,021 lines (952 loc) • 30.4 kB
text/typescript
/**
* Email Service for BackendSDK
*
* Provides email configuration management, template management,
* and email sending functionality.
*/
import crypto from "crypto";
import nodemailer from "nodemailer";
import { DatabaseConnection, Logger } from "./core";
import { normalizeError } from "./utils/error-handler";
export interface EmailConfig {
smtp_host: string;
smtp_port: number;
smtp_secure: boolean;
smtp_username: string;
smtp_password: string;
from_email: string;
from_name: string;
}
export interface EmailTemplate {
id: string;
project_id: string;
name: string;
subject: string;
body: string;
variables: string[];
created_at: string;
updated_at: string;
}
export interface EmailRequest {
project_id: string;
to: string | string[];
subject: string;
body: string;
text?: string;
cc?: string | string[];
bcc?: string | string[];
replyTo?: string;
attachments?: Array<{
filename?: string;
content?: string | Buffer;
path?: string;
contentType?: string;
}>;
}
export interface EmailResult {
success: boolean;
message: string;
messageId?: string;
error?: string;
}
export interface EmailHistory {
id: string;
project_id: string;
to: string;
subject: string;
status: "sent" | "failed" | "pending";
sent_at?: string;
error_message?: string;
template_id?: string;
created_at: string;
}
export interface EmailProvider {
id: string;
name: string;
type: "smtp" | "sendgrid" | "mailgun" | "aws_ses";
config: Record<string, unknown>;
is_active: boolean;
created_at: string;
updated_at: string;
}
/**
* Email Service for BackendSDK
*
* Provides email configuration management, template management,
* and email sending functionality.
*
* @class EmailService
* @example
* const emailService = new EmailService(dbConnection, logger);
* await emailService.sendEmail({
* project_id: 'project-id',
* to: 'user@example.com',
* subject: 'Hello',
* body: 'World'
* });
*/
export class EmailService {
private db: DatabaseConnection;
private logger: Logger;
/**
* Create a new EmailService instance
*
* @param {DatabaseConnection} databaseConnection - Database connection
* @param {Logger} logger - Logger instance
*/
constructor(databaseConnection: DatabaseConnection, logger: Logger) {
this.db = databaseConnection;
this.logger = logger;
}
/**
* Get email configuration for a project
*
* @param {string} projectId - Project ID
* @returns {Promise<EmailConfig | null>} Email configuration or null if not found
* @throws {Error} If query fails
*
* @example
* const config = await emailService.getConfig('project-id');
*/
async getConfig(projectId: string): Promise<EmailConfig | null> {
try {
const result = await this.db.query(
`SELECT settings->>'email_config' as email_config
FROM projects
WHERE id = $1`,
[projectId]
);
const row = result.rows[0] as { email_config?: string };
if (result.rows.length === 0 || !row.email_config) {
return null;
}
return JSON.parse(row.email_config);
} catch (error) {
this.logger.error("Failed to get email config:", error);
throw normalizeError(error, "INTERNAL_ERROR", {
operation: "getEmailConfig",
});
}
}
/**
* Update email configuration for a project
*
* @param {string} projectId - Project ID
* @param {EmailConfig} config - Email configuration
* @returns {Promise<EmailConfig | null>} Updated configuration or null if project not found
* @throws {Error} If update fails
*
* @example
* const config = await emailService.updateConfig('project-id', {
* smtp_host: 'smtp.example.com',
* smtp_port: 587,
* smtp_secure: false,
* smtp_username: 'user',
* smtp_password: 'pass',
* from_email: 'noreply@example.com',
* from_name: 'KRAPI'
* });
*/
async updateConfig(
projectId: string,
config: EmailConfig
): Promise<EmailConfig | null> {
try {
// Add timeout to prevent hanging
const timeoutPromise = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error("Email config update timeout")), 5000)
);
// First get current settings with timeout
const currentResult = await Promise.race([
this.db.query(
"SELECT settings FROM projects WHERE id = $1",
[projectId]
),
timeoutPromise,
]);
if (currentResult.rows.length === 0) {
return null;
}
// Parse current settings and update email_config
const currentRow = currentResult.rows[0] as { settings?: string };
let settings: Record<string, unknown> = {};
if (currentRow.settings) {
try {
settings = typeof currentRow.settings === "string"
? JSON.parse(currentRow.settings)
: currentRow.settings;
} catch {
settings = {};
}
}
settings.email_config = config;
// Update with merged settings (SQLite-compatible) with timeout
const now = new Date().toISOString();
await Promise.race([
this.db.query(
`UPDATE projects
SET settings = $1, updated_at = $2
WHERE id = $3`,
[JSON.stringify(settings), now, projectId]
),
timeoutPromise,
]);
return config;
} catch (error) {
this.logger.error("Failed to update email config:", error);
// Return null instead of throwing to prevent hanging
// This allows the application to continue even if email config update fails
return null;
}
}
/**
* Test email configuration
*
* Tests the email configuration by attempting to create a test transporter.
*
* @param {string} projectId - Project ID
* @returns {Promise<EmailResult>} Test result
* @throws {Error} If configuration not found or test fails
*
* @example
* const result = await emailService.testConfig('project-id');
* if (result.success) {
* console.log('Email configuration is valid');
* }
*/
async testConfig(projectId: string): Promise<EmailResult> {
try {
const config = await this.getConfig(projectId);
if (!config) {
return {
success: false,
message: "Email configuration not found for this project",
};
}
// Test the email configuration by creating a test transporter
const transporter = nodemailer.createTransport({
host: config.smtp_host,
port: config.smtp_port,
secure: config.smtp_secure,
auth: {
user: config.smtp_username,
pass: config.smtp_password,
},
});
// Verify the connection
await transporter.verify();
return {
success: true,
message: "Email configuration is valid and connection verified",
};
} catch (error) {
this.logger.error("Email config test failed:", error);
return {
success: false,
message:
error instanceof Error
? error.message
: "Failed to test email configuration",
};
}
}
/**
* Get all email templates for a project
*
* Retrieves all email templates associated with a project.
*
* @param {string} projectId - Project ID
* @returns {Promise<EmailTemplate[]>} Array of email templates
* @throws {Error} If query fails
*
* @example
* const templates = await emailService.getTemplates('project-id');
*/
async getTemplates(projectId: string): Promise<EmailTemplate[]> {
try {
const result = await this.db.query(
`SELECT * FROM email_templates
WHERE project_id = $1
ORDER BY created_at DESC`,
[projectId]
);
return result.rows as EmailTemplate[];
} catch (error) {
this.logger.error("Failed to get email templates:", error);
throw normalizeError(error, "INTERNAL_ERROR", {
operation: "getEmailTemplates",
});
}
}
/**
* Get email template by ID
*
* Retrieves a single email template by its ID.
*
* @param {string} templateId - Template ID
* @returns {Promise<EmailTemplate | null>} Template or null if not found
* @throws {Error} If query fails
*
* @example
* const template = await emailService.getTemplate('template-id');
*/
async getTemplate(templateId: string): Promise<EmailTemplate | null> {
try {
const result = await this.db.query(
`SELECT * FROM email_templates
WHERE id = $1`,
[templateId]
);
return result.rows.length > 0 ? (result.rows[0] as EmailTemplate) : null;
} catch (error) {
this.logger.error("Failed to get email template:", error);
throw normalizeError(error, "INTERNAL_ERROR", {
operation: "getEmailTemplate",
});
}
}
/**
* Create a new email template
*
* Creates a new email template with subject, body, and variable definitions.
*
* @param {Omit<EmailTemplate, "id" | "created_at" | "updated_at">} templateData - Template data
* @param {string} templateData.project_id - Project ID
* @param {string} templateData.name - Template name
* @param {string} templateData.subject - Email subject (supports variables)
* @param {string} templateData.body - Email body HTML (supports variables)
* @param {string[]} [templateData.variables] - Available template variables
* @returns {Promise<EmailTemplate>} Created template
* @throws {Error} If creation fails
*
* @example
* const template = await emailService.createTemplate({
* project_id: 'project-id',
* name: 'welcome',
* subject: 'Welcome {{name}}!',
* body: '<h1>Welcome {{name}}</h1><p>Your account is ready.</p>',
* variables: ['name', 'email']
* });
*/
async createTemplate(
templateData: Omit<EmailTemplate, "id" | "created_at" | "updated_at">
): Promise<EmailTemplate> {
try {
const { name, subject, body, variables, project_id } = templateData;
// Generate template ID (SQLite doesn't support RETURNING *)
const templateId = crypto.randomUUID();
const now = new Date().toISOString();
// SQLite-compatible INSERT (no RETURNING *)
await this.db.query(
`INSERT INTO email_templates (id, project_id, name, subject, body, variables, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
[templateId, project_id, name, subject, body, JSON.stringify(variables || []), now, now]
);
// Query back the inserted row
const result = await this.db.query(
"SELECT * FROM email_templates WHERE id = $1",
[templateId]
);
return result.rows[0] as EmailTemplate;
} catch (error) {
this.logger.error("Failed to create email template:", error);
throw normalizeError(error, "INTERNAL_ERROR", {
operation: "createEmailTemplate",
});
}
}
/**
* Update an email template
*
* Updates email template subject, body, or variables.
*
* @param {string} templateId - Template ID
* @param {Partial<EmailTemplate>} templateData - Template updates
* @param {string} [templateData.name] - New template name
* @param {string} [templateData.subject] - Updated subject
* @param {string} [templateData.body] - Updated body
* @param {string[]} [templateData.variables] - Updated variables
* @returns {Promise<EmailTemplate | null>} Updated template or null if not found
* @throws {Error} If update fails
*
* @example
* const updated = await emailService.updateTemplate('template-id', {
* subject: 'Updated Welcome {{name}}!',
* body: '<h1>Welcome {{name}}</h1>'
* });
*/
async updateTemplate(
templateId: string,
templateData: Partial<EmailTemplate>
): Promise<EmailTemplate | null> {
try {
const fields: string[] = [];
const values: unknown[] = [];
let paramCount = 1;
if (templateData.name !== undefined) {
fields.push(`name = $${paramCount++}`);
values.push(templateData.name);
}
if (templateData.subject !== undefined) {
fields.push(`subject = $${paramCount++}`);
values.push(templateData.subject);
}
if (templateData.body !== undefined) {
fields.push(`body = $${paramCount++}`);
values.push(templateData.body);
}
if (templateData.variables !== undefined) {
fields.push(`variables = $${paramCount++}`);
values.push(JSON.stringify(templateData.variables));
}
if (fields.length === 0) {
return this.getTemplate(templateId);
}
// SQLite-compatible: use CURRENT_TIMESTAMP instead of NOW()
fields.push(`updated_at = $${paramCount++}`);
values.push(new Date().toISOString());
values.push(templateId);
// SQLite doesn't support RETURNING *, so update and query back separately
await this.db.query(
`UPDATE email_templates
SET ${fields.join(", ")}
WHERE id = $${paramCount}`,
values
);
// Query back the updated row
const result = await this.db.query(
"SELECT * FROM email_templates WHERE id = $1",
[templateId]
);
return result.rows.length > 0 ? (result.rows[0] as EmailTemplate) : null;
} catch (error) {
this.logger.error("Failed to update email template:", error);
throw normalizeError(error, "INTERNAL_ERROR", {
operation: "updateEmailTemplate",
});
}
}
/**
* Delete an email template
*
* Permanently deletes an email template.
*
* @param {string} templateId - Template ID
* @returns {Promise<boolean>} True if deletion successful
* @throws {Error} If deletion fails
*
* @example
* const deleted = await emailService.deleteTemplate('template-id');
*/
async deleteTemplate(templateId: string): Promise<boolean> {
try {
const result = await this.db.query(
`DELETE FROM email_templates
WHERE id = $1`,
[templateId]
);
return result.rowCount > 0;
} catch (error) {
this.logger.error("Failed to delete email template:", error);
throw normalizeError(error, "INTERNAL_ERROR", {
operation: "deleteEmailTemplate",
});
}
}
/**
* Send an email
*
* Sends an email using the project's email configuration.
* Validates configuration and required fields before sending.
*
* @param {EmailRequest} emailData - Email data
* @param {string} emailData.project_id - Project ID
* @param {string | string[]} emailData.to - Recipient email(s)
* @param {string} emailData.subject - Email subject
* @param {string} emailData.body - Email body (HTML)
* @param {string} [emailData.text] - Plain text version
* @param {string | string[]} [emailData.cc] - CC recipients
* @param {string | string[]} [emailData.bcc] - BCC recipients
* @param {string} [emailData.replyTo] - Reply-to address
* @param {Array} [emailData.attachments] - Email attachments
* @returns {Promise<EmailResult>} Email sending result
* @throws {Error} If sending fails or configuration missing
*
* @example
* const result = await emailService.sendEmail({
* project_id: 'project-id',
* to: 'user@example.com',
* subject: 'Hello',
* body: '<h1>Hello World</h1>'
* });
*/
async sendEmail(emailData: EmailRequest): Promise<EmailResult> {
try {
// Get the project to check if email is configured
const projectResult = await this.db.query(
"SELECT id FROM projects WHERE id = $1",
[emailData.project_id]
);
if (projectResult.rows.length === 0) {
return {
success: false,
message: "Project not found",
};
}
// Check if email configuration exists
const emailConfig = await this.getConfig(emailData.project_id);
if (!emailConfig) {
return {
success: false,
message: "Email configuration not found for this project",
};
}
// Validate required email data
const { to, subject, body } = emailData;
if (!to || !subject || !body) {
return {
success: false,
message: "To, subject, and body are required for sending emails",
};
}
// Create transporter and send email
const transporter = nodemailer.createTransport({
host: emailConfig.smtp_host,
port: emailConfig.smtp_port,
secure: emailConfig.smtp_secure,
auth: {
user: emailConfig.smtp_username,
pass: emailConfig.smtp_password,
},
});
const result = await transporter.sendMail({
from: `"${emailConfig.from_name}" <${emailConfig.from_email}>`,
to: Array.isArray(to) ? to.join(", ") : to,
subject,
html: body,
text: emailData.text || body,
cc: emailData.cc,
bcc: emailData.bcc,
replyTo: emailData.replyTo,
attachments: emailData.attachments,
});
return {
success: true,
message: `Email sent successfully. Message ID: ${
result.messageId || "unknown"
}`,
messageId: result.messageId,
};
} catch (error) {
this.logger.error("Send email error:", error);
return {
success: false,
message:
error instanceof Error ? error.message : "Failed to send email",
error: error instanceof Error ? error.message : "Unknown error",
};
}
}
/**
* Send email (convenience method)
*
* Alias for sendEmail. Sends an email with project context.
*
* @param {EmailRequest} emailWithProject - Email request with project_id
* @returns {Promise<EmailResult>} Email sending result
* @throws {Error} If sending fails
*
* @example
* const result = await emailService.sendEmailRequest({
* project_id: 'project-id',
* to: 'user@example.com',
* subject: 'Hello',
* body: 'World'
* });
*/
async sendEmailRequest(emailWithProject: EmailRequest): Promise<EmailResult> {
return this.sendEmail(emailWithProject);
}
/**
* Send email using a template
*
* @param {string} projectId - Project ID
* @param {string} templateId - Template ID
* @param {Object} emailData - Email data with variables
* @returns {Promise<{ success: boolean; data?: { message_id?: string; sent_at: string } }>} Send result
*/
async sendTemplateEmail(
projectId: string,
templateId: string,
emailData: {
to: string | string[];
variables?: Record<string, unknown>;
cc?: string | string[];
bcc?: string | string[];
subject?: string;
from_email?: string;
from_name?: string;
}
): Promise<{ success: boolean; data?: { message_id?: string; sent_at: string } }> {
try {
const template = await this.getTemplate(templateId);
if (!template) {
return { success: false };
}
// Replace template variables
let subject = emailData.subject || template.subject;
let body = template.body;
if (emailData.variables) {
Object.entries(emailData.variables).forEach(([key, value]) => {
const regex = new RegExp(`{{${key}}}`, "g");
subject = subject.replace(regex, String(value));
body = body.replace(regex, String(value));
});
}
const emailRequest: EmailRequest = {
project_id: projectId,
to: emailData.to,
subject,
body,
};
if (emailData.cc !== undefined) {
emailRequest.cc = emailData.cc;
}
if (emailData.bcc !== undefined) {
emailRequest.bcc = emailData.bcc;
}
const result = await this.sendEmail(emailRequest);
const response: {
success: boolean;
data?: { message_id?: string; sent_at: string };
} = {
success: result.success,
data: {
sent_at: new Date().toISOString(),
},
};
if (result.messageId !== undefined) {
if (response.data) {
response.data.message_id = result.messageId;
}
}
return response;
} catch (error) {
this.logger.error("Failed to send template email:", error);
return { success: false };
}
}
/**
* Send bulk emails
*
* @param {string} projectId - Project ID
* @param {Object} bulkRequest - Bulk email request
* @returns {Promise<{ success: boolean; data?: { message_ids: string[]; sent_at: string } }>} Send result
*/
async sendBulkEmail(
projectId: string,
bulkRequest: {
template_id?: string;
recipients: Array<{
email: string;
name?: string;
variables?: Record<string, unknown>;
}>;
subject?: string;
from_email?: string;
from_name?: string;
scheduled_at?: string;
}
): Promise<{ success: boolean; data?: { message_ids: string[]; sent_at: string } }> {
try {
const messageIds: string[] = [];
const config = await this.getConfig(projectId);
if (!config) {
return { success: false };
}
for (const recipient of bulkRequest.recipients) {
let subject = bulkRequest.subject || "Email";
let body = "Email body";
if (bulkRequest.template_id) {
const template = await this.getTemplate(bulkRequest.template_id);
if (template) {
subject = template.subject;
body = template.body;
if (recipient.variables) {
Object.entries(recipient.variables).forEach(([key, value]) => {
const regex = new RegExp(`{{${key}}}`, "g");
subject = subject.replace(regex, String(value));
body = body.replace(regex, String(value));
});
}
}
}
const result = await this.sendEmail({
project_id: projectId,
to: recipient.email,
subject,
body,
});
if (result.messageId) {
messageIds.push(result.messageId);
}
}
return {
success: true,
data: {
message_ids: messageIds,
sent_at: new Date().toISOString(),
},
};
} catch (error) {
this.logger.error("Failed to send bulk email:", error);
return { success: false };
}
}
/**
* Get email history
*
* @param {string} projectId - Project ID
* @param {Object} options - Query options
* @returns {Promise<{ success: boolean; data?: EmailHistory[] }>} Email history
*/
async getEmailHistory(
projectId: string,
options?: {
limit?: number;
offset?: number;
status?: "sent" | "failed" | "bounced" | "delivered";
recipient?: string;
template_id?: string;
sent_after?: string;
sent_before?: string;
}
): Promise<{ success: boolean; data?: EmailHistory[] }> {
try {
let query = `SELECT * FROM email_history WHERE project_id = $1`;
const params: unknown[] = [projectId];
let paramCount = 2;
if (options?.status) {
query += ` AND status = $${paramCount++}`;
params.push(options.status);
}
if (options?.recipient) {
query += ` AND "to" = $${paramCount++}`;
params.push(options.recipient);
}
if (options?.template_id) {
query += ` AND template_id = $${paramCount++}`;
params.push(options.template_id);
}
if (options?.sent_after) {
query += ` AND sent_at >= $${paramCount++}`;
params.push(options.sent_after);
}
if (options?.sent_before) {
query += ` AND sent_at <= $${paramCount++}`;
params.push(options.sent_before);
}
query += ` ORDER BY sent_at DESC`;
if (options?.limit) {
query += ` LIMIT $${paramCount++}`;
params.push(options.limit);
}
if (options?.offset) {
query += ` OFFSET $${paramCount++}`;
params.push(options.offset);
}
const result = await this.db.query(query, params);
return {
success: true,
data: result.rows as EmailHistory[],
};
} catch (error) {
this.logger.error("Failed to get email history:", error);
return { success: false, data: [] };
}
}
/**
* Get email details
*
* @param {string} projectId - Project ID
* @param {string} messageId - Message ID
* @returns {Promise<{ success: boolean; data?: EmailHistory }>} Email details
*/
async getEmailDetails(
projectId: string,
messageId: string
): Promise<{ success: boolean; data?: EmailHistory }> {
try {
const result = await this.db.query(
`SELECT * FROM email_history WHERE project_id = $1 AND id = $2`,
[projectId, messageId]
);
const response: {
success: boolean;
data?: EmailHistory;
} = {
success: true,
};
if (result.rows.length > 0) {
response.data = result.rows[0] as EmailHistory;
}
return response;
} catch (error) {
this.logger.error("Failed to get email details:", error);
return { success: false };
}
}
/**
* Get email analytics
*
* @param {string} projectId - Project ID
* @param {Object} options - Analytics options
* @returns {Promise<{ success: boolean; data?: unknown }>} Analytics data
*/
async getEmailAnalytics(
projectId: string,
options?: {
period?: "day" | "week" | "month" | "year";
start_date?: string;
end_date?: string;
template_id?: string;
}
): Promise<{ success: boolean; data?: unknown }> {
try {
// Basic analytics implementation
const historyOptions: {
limit?: number;
offset?: number;
status?: "failed" | "sent" | "bounced" | "delivered";
recipient?: string;
template_id?: string;
sent_after?: string;
sent_before?: string;
} = {};
if (options?.start_date) {
historyOptions.sent_after = options.start_date;
}
if (options?.end_date) {
historyOptions.sent_before = options.end_date;
}
if (options?.template_id) {
historyOptions.template_id = options.template_id;
}
const history = await this.getEmailHistory(projectId, historyOptions);
const emails = history.data || [];
const totalSent = emails.length;
const totalDelivered = emails.filter((e) => e.status === "sent").length;
const totalBounced = emails.filter((e) => e.status === "failed").length;
return {
success: true,
data: {
period: options?.period || "month",
start_date: options?.start_date || new Date().toISOString(),
end_date: options?.end_date || new Date().toISOString(),
total_sent: totalSent,
total_delivered: totalDelivered,
total_bounced: totalBounced,
total_opened: 0,
total_clicked: 0,
total_complained: 0,
delivery_rate: totalSent > 0 ? totalDelivered / totalSent : 0,
open_rate: 0,
click_rate: 0,
bounce_rate: totalSent > 0 ? totalBounced / totalSent : 0,
complaint_rate: 0,
daily_stats: [],
},
};
} catch (error) {
this.logger.error("Failed to get email analytics:", error);
return { success: false };
}
}
/**
* Unsubscribe email
*
* @param {string} projectId - Project ID
* @param {string} email - Email address to unsubscribe
* @returns {Promise<{ success: boolean; message?: string }>} Unsubscribe result
*/
async unsubscribeEmail(
projectId: string,
email: string
): Promise<{ success: boolean; message?: string }> {
try {
// Add to unsubscribe list (SQLite-compatible)
const now = new Date().toISOString();
await this.db.query(
`INSERT INTO email_unsubscribes (project_id, email, created_at)
VALUES ($1, $2, $3)
ON CONFLICT (project_id, email) DO NOTHING`,
[projectId, email, now]
);
return {
success: true,
message: "Email unsubscribed successfully",
};
} catch (error) {
this.logger.error("Failed to unsubscribe email:", error);
return {
success: false,
message: error instanceof Error ? error.message : "Failed to unsubscribe",
};
}
}
/**
* Validate email address
*
* @param {string} email - Email address to validate
* @returns {Promise<{ success: boolean; data?: unknown }>} Validation result
*/
async validateEmail(
email: string
): Promise<{
success: boolean;
data?: {
valid: boolean;
format_valid: boolean;
domain_valid: boolean;
disposable: boolean;
role: boolean;
free_email: boolean;
suggestions?: string[];
};
}> {
try {
// Basic email validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const formatValid = emailRegex.test(email);
const domain = email.split("@")[1];
const domainValid = formatValid && domain && domain.includes(".");
const emailParts = email.split("@");
const localPart = emailParts[0];
const domainLower = domain?.toLowerCase();
return {
success: true,
data: {
valid: Boolean(formatValid && domainValid),
format_valid: Boolean(formatValid),
domain_valid: Boolean(domainValid),
disposable: false,
role: Boolean(localPart && (localPart.toLowerCase().includes("noreply") || localPart.toLowerCase().includes("no-reply"))),
free_email: Boolean(domainLower && ["gmail.com", "yahoo.com", "hotmail.com", "outlook.com"].includes(domainLower)),
},
};
} catch (error) {
this.logger.error("Failed to validate email:", error);
return {
success: false,
data: {
valid: false,
format_valid: false,
domain_valid: false,
disposable: false,
role: false,
free_email: false,
},
};
}
}
}