@smartsamurai/krapi-sdk
Version:
KRAPI TypeScript SDK - Easy-to-use client SDK for connecting to self-hosted KRAPI servers (like Appwrite SDK)
637 lines (606 loc) • 22 kB
text/typescript
/**
* Email Adapter
*
* Unifies EmailHttpClient and EmailService behind a common interface.
*/
import {
EmailService,
EmailConfig as ServiceEmailConfig,
EmailTemplate as ServiceEmailTemplate,
EmailRequest as ServiceEmailRequest,
} from "../../email-service";
import { EmailHttpClient } from "../../http-clients/email-http-client";
import { EmailConfig, EmailTemplate, SendEmailRequest } from "../../types";
import { createAdapterInitError } from "./error-handler";
type Mode = "client" | "server";
/**
* Transform service EmailConfig (smtp_username) to types EmailConfig (smtp_user)
*/
function transformServiceEmailConfigToTypes(
serviceConfig: ServiceEmailConfig | null
): EmailConfig {
if (!serviceConfig) {
return {
smtp_host: "",
smtp_port: 587,
smtp_user: "",
smtp_password: "",
smtp_secure: false,
from_email: "",
from_name: "",
enabled: true,
};
}
return {
smtp_host: serviceConfig.smtp_host,
smtp_port: serviceConfig.smtp_port,
smtp_user: serviceConfig.smtp_username,
smtp_password: serviceConfig.smtp_password,
smtp_secure: serviceConfig.smtp_secure,
from_email: serviceConfig.from_email,
from_name: serviceConfig.from_name,
enabled: true,
};
}
/**
* Transform service EmailTemplate to types EmailTemplate
*/
function transformServiceEmailTemplateToTypes(
serviceTemplate: ServiceEmailTemplate
): EmailTemplate {
return {
id: serviceTemplate.id,
name: serviceTemplate.name,
subject: serviceTemplate.subject,
body: serviceTemplate.body,
type: "custom", // Default type since service doesn't have it
project_id: serviceTemplate.project_id,
variables: serviceTemplate.variables || [],
created_at: serviceTemplate.created_at,
updated_at: serviceTemplate.updated_at,
};
}
/**
* Transform types SendEmailRequest to service EmailRequest
*/
function transformTypesEmailRequestToService(
emailData: SendEmailRequest,
projectId: string
): ServiceEmailRequest {
const request: ServiceEmailRequest = {
project_id: projectId,
to: emailData.to,
subject: emailData.subject,
body: emailData.body || "",
text: emailData.body || "",
};
if (emailData.reply_to) {
request.replyTo = emailData.reply_to;
}
if (emailData.attachments) {
request.attachments = emailData.attachments.map((att) => {
const attachment: {
filename?: string;
content?: string | Buffer;
path?: string;
contentType?: string;
} = {};
if (att.filename) attachment.filename = att.filename;
if (att.content) attachment.content = att.content;
if (att.content_type) attachment.contentType = att.content_type;
return attachment;
});
}
return request;
}
/**
* Transform types SendEmailRequest to EmailRequest (for HTTP client)
*/
function transformTypesToHttpEmailRequest(
emailData: SendEmailRequest,
projectId: string
): {
project_id: string;
to: string | string[];
subject: string;
body: string;
replyTo?: string;
template_id?: string;
variables?: Record<string, unknown>;
from?: string;
attachments?: Array<{
filename: string;
content: string | Buffer;
content_type?: string;
}>;
} {
// For HTTP client, we need to create a compatible format
// Since EmailRequest and SendEmailRequest have different structures,
// we'll build an object that matches what the HTTP client expects
const request: {
project_id: string;
to: string | string[];
subject: string;
body: string;
replyTo?: string;
template_id?: string;
variables?: Record<string, unknown>;
from?: string;
attachments?: Array<{
filename: string;
content: string | Buffer;
content_type?: string;
}>;
} = {
project_id: projectId,
to: emailData.to,
subject: emailData.subject,
body: emailData.body || "",
};
if (emailData.reply_to) {
request.replyTo = emailData.reply_to;
}
if (emailData.template_id) {
request.template_id = emailData.template_id;
}
if (emailData.variables) {
request.variables = emailData.variables;
}
if (emailData.from) {
request.from = emailData.from;
}
if (emailData.attachments) {
request.attachments = emailData.attachments;
}
return request;
}
export class EmailAdapter {
private mode: Mode;
private httpClient: EmailHttpClient | undefined;
private service: EmailService | undefined;
constructor(mode: Mode, httpClient?: EmailHttpClient, service?: EmailService) {
this.mode = mode;
this.httpClient = httpClient;
this.service = service;
}
async getConfig(projectId: string): Promise<EmailConfig> {
if (this.mode === "client") {
if (!this.httpClient) {
throw createAdapterInitError("HTTP client", this.mode);
}
const response = await this.httpClient.getConfig(projectId);
return (response.data as unknown as EmailConfig) || ({} as EmailConfig);
} else {
if (!this.service) {
throw createAdapterInitError("Email service", this.mode);
}
const config = await this.service.getConfig(projectId);
return transformServiceEmailConfigToTypes(config);
}
}
async updateConfig(projectId: string, config: {
provider: string;
settings?: Record<string, unknown>;
}): Promise<EmailConfig> {
if (this.mode === "client") {
if (!this.httpClient) {
throw createAdapterInitError("HTTP client", this.mode);
}
// Transform interface format to EmailConfig format for HTTP client
// Ensure smtp_host and smtp_port are always set from the request body
// Support multiple field name formats: host/hostname, port, smtp_host/smtpHost, smtp_port/smtpPort
// SAFE: Check that config.settings exists before accessing nested properties
const settings = config.settings || {};
const smtpHost = (typeof settings.smtp_host === "string" && settings.smtp_host)
? settings.smtp_host
: (typeof settings.smtpHost === "string" && settings.smtpHost)
? settings.smtpHost
: (typeof settings.host === "string" && settings.host)
? settings.host
: (typeof settings.hostname === "string" && settings.hostname)
? settings.hostname
: "";
const smtpPort = (typeof settings.smtp_port === "number" && settings.smtp_port)
? settings.smtp_port
: (typeof settings.smtpPort === "number" && settings.smtpPort)
? settings.smtpPort
: (typeof settings.port === "number" && settings.port)
? settings.port
: 587;
const emailConfig: Partial<EmailConfig> = {
smtp_host: smtpHost,
smtp_port: smtpPort,
smtp_user: (typeof settings.smtp_user === "string" && settings.smtp_user)
? settings.smtp_user
: (typeof settings.smtpUser === "string" && settings.smtpUser)
? settings.smtpUser
: (typeof settings.smtp_username === "string" && settings.smtp_username)
? settings.smtp_username
: (typeof settings.smtpUsername === "string" && settings.smtpUsername)
? settings.smtpUsername
: "",
smtp_password: (typeof settings.smtp_password === "string" && settings.smtp_password)
? settings.smtp_password
: (typeof settings.smtpPassword === "string" && settings.smtpPassword)
? settings.smtpPassword
: "",
smtp_secure: (typeof settings.smtp_secure === "boolean")
? settings.smtp_secure
: (typeof settings.smtpSecure === "boolean")
? settings.smtpSecure
: false,
from_email: (typeof settings.from_email === "string" && settings.from_email)
? settings.from_email
: (typeof settings.fromEmail === "string" && settings.fromEmail)
? settings.fromEmail
: "",
from_name: (typeof settings.from_name === "string" && settings.from_name)
? settings.from_name
: (typeof settings.fromName === "string" && settings.fromName)
? settings.fromName
: "",
enabled: (typeof settings.enabled === "boolean") ? settings.enabled : true,
};
const response = await this.httpClient.updateConfig(projectId, emailConfig);
// Response interceptor returns response.data, so response is already unwrapped
// Handle both formats: EmailConfig directly or { success: true, data: EmailConfig }
if (response && typeof response === "object") {
// If response has 'data' field, extract it
if ("data" in response && response.data) {
return (response.data as unknown as EmailConfig) || ({} as EmailConfig);
}
// If response is EmailConfig directly (has smtp_host, etc.)
if ("smtp_host" in response || "smtpHost" in response) {
return response as unknown as EmailConfig;
}
}
return {} as EmailConfig;
} else {
if (!this.service) {
throw createAdapterInitError("Email service", this.mode);
}
// Transform interface format to service EmailConfig format
// Ensure smtp_host and smtp_port are always set from the request body
// Support multiple field name formats: host/hostname, port, smtp_host/smtpHost, smtp_port/smtpPort
// SAFE: Check that config.settings exists before accessing nested properties
const settings = config.settings || {};
const smtpHost = (typeof settings.smtp_host === "string" && settings.smtp_host)
? settings.smtp_host
: (typeof settings.smtpHost === "string" && settings.smtpHost)
? settings.smtpHost
: (typeof settings.host === "string" && settings.host)
? settings.host
: (typeof settings.hostname === "string" && settings.hostname)
? settings.hostname
: "";
const smtpPort = (typeof settings.smtp_port === "number" && settings.smtp_port)
? settings.smtp_port
: (typeof settings.smtpPort === "number" && settings.smtpPort)
? settings.smtpPort
: (typeof settings.port === "number" && settings.port)
? settings.port
: 587;
const serviceConfig: ServiceEmailConfig = {
smtp_host: smtpHost,
smtp_port: smtpPort,
smtp_username: (typeof settings.smtp_username === "string" && settings.smtp_username)
? settings.smtp_username
: (typeof settings.smtpUsername === "string" && settings.smtpUsername)
? settings.smtpUsername
: (typeof settings.smtp_user === "string" && settings.smtp_user)
? settings.smtp_user
: (typeof settings.smtpUser === "string" && settings.smtpUser)
? settings.smtpUser
: "",
smtp_password: (typeof settings.smtp_password === "string" && settings.smtp_password)
? settings.smtp_password
: (typeof settings.smtpPassword === "string" && settings.smtpPassword)
? settings.smtpPassword
: "",
smtp_secure: (typeof settings.smtp_secure === "boolean")
? settings.smtp_secure
: (typeof settings.smtpSecure === "boolean")
? settings.smtpSecure
: false,
from_email: (typeof settings.from_email === "string" && settings.from_email)
? settings.from_email
: (typeof settings.fromEmail === "string" && settings.fromEmail)
? settings.fromEmail
: "",
from_name: (typeof settings.from_name === "string" && settings.from_name)
? settings.from_name
: (typeof settings.fromName === "string" && settings.fromName)
? settings.fromName
: "",
};
const updated = await this.service.updateConfig(projectId, serviceConfig);
// If service returns null, return a config with the values we extracted from request
if (!updated) {
return {
smtp_host: smtpHost,
smtp_port: smtpPort,
smtp_user: serviceConfig.smtp_username,
smtp_password: serviceConfig.smtp_password,
smtp_secure: serviceConfig.smtp_secure,
from_email: serviceConfig.from_email,
from_name: serviceConfig.from_name,
enabled: true,
};
}
return transformServiceEmailConfigToTypes(updated);
}
}
async testConfig(projectId: string, testEmail: string): Promise<{ success: boolean; message?: string }> {
if (this.mode === "client") {
if (!this.httpClient) {
throw createAdapterInitError("HTTP client", this.mode);
}
const response = await this.httpClient.testConfig(projectId, testEmail);
return response.data || { success: false };
} else {
if (!this.service) {
throw createAdapterInitError("Email service", this.mode);
}
const result = await this.service.testConfig(projectId);
return {
success: result.success || false,
message: result.message,
};
}
}
async getTemplates(projectId: string, options?: {
limit?: number;
offset?: number;
search?: string;
}): Promise<EmailTemplate[]> {
if (this.mode === "client") {
if (!this.httpClient) {
throw createAdapterInitError("HTTP client", this.mode);
}
const response = await this.httpClient.getTemplates(projectId, options);
const data = response.data;
if (data && "data" in data) {
return (data.data as EmailTemplate[]) || [];
}
return (data as unknown as EmailTemplate[]) || [];
} else {
if (!this.service) {
throw createAdapterInitError("Email service", this.mode);
}
const templates = await this.service.getTemplates(projectId);
// Transform service EmailTemplate to types EmailTemplate format
return templates.map(transformServiceEmailTemplateToTypes);
}
}
async getTemplate(projectId: string, templateId: string): Promise<EmailTemplate> {
if (this.mode === "client") {
if (!this.httpClient) {
throw createAdapterInitError("HTTP client", this.mode);
}
const response = await this.httpClient.getTemplate(projectId, templateId);
return (response.data as unknown as EmailTemplate) || ({} as EmailTemplate);
} else {
if (!this.service) {
throw createAdapterInitError("Email service", this.mode);
}
const template = await this.service.getTemplate(templateId);
if (!template) {
return {} as EmailTemplate;
}
return transformServiceEmailTemplateToTypes(template);
}
}
async createTemplate(projectId: string, template: {
name: string;
subject: string;
body: string;
variables: string[];
type?: string;
}): Promise<EmailTemplate> {
if (this.mode === "client") {
if (!this.httpClient) {
throw createAdapterInitError("HTTP client", this.mode);
}
const response = await this.httpClient.createTemplate(projectId, template);
return (response.data as unknown as EmailTemplate) || ({} as EmailTemplate);
} else {
if (!this.service) {
throw createAdapterInitError("Email service", this.mode);
}
const serviceTemplate: {
name: string;
subject: string;
body: string;
variables: string[];
project_id: string;
} = {
name: template.name,
subject: template.subject,
body: template.body,
variables: template.variables,
project_id: projectId,
};
const created = await this.service.createTemplate(serviceTemplate);
return transformServiceEmailTemplateToTypes(created);
}
}
async updateTemplate(projectId: string, templateId: string, updates: Partial<EmailTemplate>): Promise<EmailTemplate> {
if (this.mode === "client") {
if (!this.httpClient) {
throw createAdapterInitError("HTTP client", this.mode);
}
const response = await this.httpClient.updateTemplate(projectId, templateId, updates);
return (response.data as unknown as EmailTemplate) || ({} as EmailTemplate);
} else {
if (!this.service) {
throw createAdapterInitError("Email service", this.mode);
}
const serviceUpdates: Partial<ServiceEmailTemplate> = {};
if (updates.name !== undefined) serviceUpdates.name = updates.name;
if (updates.subject !== undefined) serviceUpdates.subject = updates.subject;
if (updates.body !== undefined) serviceUpdates.body = updates.body;
if (updates.variables !== undefined) serviceUpdates.variables = updates.variables;
const updated = await this.service.updateTemplate(templateId, serviceUpdates);
if (!updated) {
return {} as EmailTemplate;
}
return transformServiceEmailTemplateToTypes(updated);
}
}
async deleteTemplate(projectId: string, templateId: string): Promise<{ success: boolean }> {
if (this.mode === "client") {
if (!this.httpClient) {
throw createAdapterInitError("HTTP client", this.mode);
}
const response = await this.httpClient.deleteTemplate(projectId, templateId);
return response.data || { success: false };
} else {
if (!this.service) {
throw createAdapterInitError("Email service", this.mode);
}
const success = await this.service.deleteTemplate(templateId);
return { success };
}
}
async send(projectId: string, emailData: SendEmailRequest): Promise<{
success: boolean;
message_id?: string;
error?: string;
}> {
if (this.mode === "client") {
if (!this.httpClient) {
throw createAdapterInitError("HTTP client", this.mode);
}
const httpEmailData = transformTypesToHttpEmailRequest(emailData, projectId);
const response = await this.httpClient.sendEmail(projectId, httpEmailData);
const data = response.data;
const result: {
success: boolean;
message_id?: string;
error?: string;
} = {
success: data?.success || false,
};
if (data?.message_id) {
result.message_id = data.message_id;
}
return result;
} else {
if (!this.service) {
throw createAdapterInitError("Email service", this.mode);
}
const emailRequest = transformTypesEmailRequestToService(emailData, projectId);
const result = await this.service.sendEmail(emailRequest);
const response: {
success: boolean;
message_id?: string;
error?: string;
} = {
success: result.success || false,
};
if (result.messageId) {
response.message_id = result.messageId;
}
if (result.message) {
response.error = result.message;
}
return response;
}
}
async getHistory(
projectId: string,
options?: {
limit?: number;
offset?: number;
status?: string;
start_date?: string;
end_date?: string;
}
): Promise<
{
id: string;
to: string;
subject: string;
status: string;
sent_at: string;
}[]
> {
if (this.mode === "client") {
if (!this.httpClient) {
throw createAdapterInitError("HTTP client", this.mode);
}
// Transform options to match HTTP client interface
const httpOptions: {
limit?: number;
offset?: number;
status?: "sent" | "failed" | "bounced" | "delivered";
sent_after?: string;
sent_before?: string;
} = {};
if (options?.limit) httpOptions.limit = options.limit;
if (options?.offset) httpOptions.offset = options.offset;
if (options?.status) {
const statusMap: Record<string, "sent" | "failed" | "bounced" | "delivered"> = {
sent: "sent",
failed: "failed",
bounced: "bounced",
delivered: "delivered",
};
httpOptions.status = statusMap[options.status] || "sent";
}
if (options?.start_date) httpOptions.sent_after = options.start_date;
if (options?.end_date) httpOptions.sent_before = options.end_date;
const response = await this.httpClient.getEmailHistory(projectId, httpOptions);
const data = response.data;
if (data && "data" in data) {
return (data.data as Array<{
id: string;
to: string;
subject: string;
status: string;
sent_at: string;
}>) || [];
}
return (data as unknown as Array<{
id: string;
to: string;
subject: string;
status: string;
sent_at: string;
}>) || [];
} else {
if (!this.service) {
throw createAdapterInitError("Email service", this.mode);
}
// Transform options to match service interface
const serviceOptions: {
limit?: number;
offset?: number;
status?: "sent" | "failed" | "bounced" | "delivered";
sent_after?: string;
sent_before?: string;
} = {};
if (options?.limit) serviceOptions.limit = options.limit;
if (options?.offset) serviceOptions.offset = options.offset;
if (options?.status) {
const statusMap: Record<string, "sent" | "failed" | "bounced" | "delivered"> = {
sent: "sent",
failed: "failed",
bounced: "bounced",
delivered: "delivered",
};
serviceOptions.status = statusMap[options.status] || "sent";
}
if (options?.start_date) serviceOptions.sent_after = options.start_date;
if (options?.end_date) serviceOptions.sent_before = options.end_date;
const history = await this.service.getEmailHistory(projectId, serviceOptions);
return (history.data as unknown as Array<{
id: string;
to: string;
subject: string;
status: string;
sent_at: string;
}>) || [];
}
}
}