Files
simstudioai--sim/apps/sim/tools/emailbison/utils.ts
T
wehub-resource-sync d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

480 lines
16 KiB
TypeScript

import { filterUndefined, isRecordLike } from '@sim/utils/object'
import type {
EmailBisonBaseParams,
EmailBisonCampaign,
EmailBisonCampaignTag,
EmailBisonLead,
EmailBisonLeadStats,
EmailBisonReply,
EmailBisonReplyAddress,
EmailBisonReplyAttachment,
EmailBisonTag,
} from '@/tools/emailbison/types'
import type { OutputProperty, ToolConfig } from '@/tools/types'
type QueryValue = string | number | boolean | Array<string | number> | undefined | null
interface EmailBisonEnvelope<T> {
data?: T
}
export function emailBisonHeaders(params: EmailBisonBaseParams): Record<string, string> {
return {
Authorization: `Bearer ${params.apiKey.trim()}`,
'Content-Type': 'application/json',
}
}
export const emailBisonBaseParamFields = {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Email Bison API token',
},
apiBaseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Email Bison instance URL that issued the token',
},
} satisfies ToolConfig['params']
export function emailBisonUrl(
path: string,
query: Record<string, QueryValue>,
baseUrl: string
): string {
const url = new URL(path, normalizeEmailBisonBaseUrl(baseUrl))
Object.entries(query).forEach(([key, value]) => {
if (value === undefined || value === null || value === '') return
if (Array.isArray(value)) {
value.forEach((item) => {
url.searchParams.append(key, String(item))
})
return
}
url.searchParams.set(key, String(value))
})
return url.toString()
}
function normalizeEmailBisonBaseUrl(baseUrl: string): string {
const trimmedBaseUrl = baseUrl.trim()
if (!trimmedBaseUrl) {
throw new Error('Email Bison Instance URL is required')
}
const rawBaseUrl = /^https?:\/\//i.test(trimmedBaseUrl)
? trimmedBaseUrl
: `https://${trimmedBaseUrl}`
const parsed = new URL(rawBaseUrl)
if (parsed.protocol !== 'https:') {
throw new Error('Email Bison Instance URL must use HTTPS')
}
return parsed.origin
}
export function jsonBody(fields: Record<string, unknown>): Record<string, unknown> {
return filterUndefined(fields)
}
async function emailBisonData<T>(response: Response): Promise<T | null> {
const payload = (await response.json()) as EmailBisonEnvelope<T>
return payload.data ?? null
}
export async function emailBisonArrayData(response: Response, label: string): Promise<unknown[]> {
const data = await emailBisonData<unknown[]>(response)
if (!Array.isArray(data)) {
throw new Error(`Email Bison response did not include a valid ${label} array`)
}
return data
}
export async function emailBisonRecordData(response: Response, label: string): Promise<unknown> {
const data = await emailBisonData<unknown>(response)
if (!isRecordLike(data)) {
throw new Error(`Email Bison response did not include a valid ${label} object`)
}
return data
}
export function mapLead(value: unknown): EmailBisonLead {
const record = toRecord(value)
const stats = toRecord(record.overall_stats)
return {
id: toNullableNumber(record.id),
first_name: toStringOrNull(record.first_name),
last_name: toStringOrNull(record.last_name),
email: toStringOrNull(record.email),
title: toStringOrNull(record.title),
company: toStringOrNull(record.company),
notes: toStringOrNull(record.notes),
status: toStringOrNull(record.status),
custom_variables: toArray(record.custom_variables).map((item) => {
const variable = toRecord(item)
return {
name: toStringOrNull(variable.name),
value: toStringOrNull(variable.value),
}
}),
lead_campaign_data: toArray(record.lead_campaign_data),
overall_stats: mapLeadStats(stats),
created_at: toStringOrNull(record.created_at),
updated_at: toStringOrNull(record.updated_at),
}
}
export function mapCampaign(value: unknown): EmailBisonCampaign {
const record = toRecord(value)
return {
id: toNullableNumber(record.id),
uuid: toStringOrNull(record.uuid),
name: toStringOrNull(record.name),
type: toStringOrNull(record.type),
status: toStringOrNull(record.status),
emails_sent: toNullableNumber(record.emails_sent),
opened: toNullableNumber(record.opened),
unique_opens: toNullableNumber(record.unique_opens),
replied: toNullableNumber(record.replied),
unique_replies: toNullableNumber(record.unique_replies),
bounced: toNullableNumber(record.bounced),
unsubscribed: toNullableNumber(record.unsubscribed),
interested: toNullableNumber(record.interested),
total_leads_contacted: toNullableNumber(record.total_leads_contacted),
total_leads: toNullableNumber(record.total_leads),
max_emails_per_day: toNullableNumber(record.max_emails_per_day),
max_new_leads_per_day: toNullableNumber(record.max_new_leads_per_day),
plain_text: toNullableBoolean(record.plain_text),
open_tracking: toNullableBoolean(record.open_tracking),
can_unsubscribe: toNullableBoolean(record.can_unsubscribe),
unsubscribe_text: toStringOrNull(record.unsubscribe_text),
...(record.sequence_prioritization !== undefined && {
sequence_prioritization: toStringOrNull(record.sequence_prioritization),
}),
tags: toArray(record.tags).map(mapCampaignTag),
created_at: toStringOrNull(record.created_at),
updated_at: toStringOrNull(record.updated_at),
}
}
function mapCampaignTag(value: unknown): EmailBisonCampaignTag {
const record = toRecord(value)
return {
id: toNullableNumber(record.id),
name: toStringOrNull(record.name),
default: toNullableBoolean(record.default),
}
}
export function mapTag(value: unknown): EmailBisonTag {
const record = toRecord(value)
return {
id: toNullableNumber(record.id),
name: toStringOrNull(record.name),
default: toNullableBoolean(record.default),
created_at: toStringOrNull(record.created_at),
updated_at: toStringOrNull(record.updated_at),
}
}
export function mapReply(value: unknown): EmailBisonReply {
const record = toRecord(value)
return {
id: toNullableNumber(record.id),
uuid: toStringOrNull(record.uuid),
folder: toStringOrNull(record.folder),
subject: toStringOrNull(record.subject),
read: toNullableBoolean(record.read),
interested: toNullableBoolean(record.interested),
automated_reply: toNullableBoolean(record.automated_reply),
html_body: toStringOrNull(record.html_body),
text_body: toStringOrNull(record.text_body),
raw_body: toStringOrNull(record.raw_body),
headers: toStringOrNull(record.headers),
date_received: toStringOrNull(record.date_received),
type: toStringOrNull(record.type),
tracked_reply: toNullableBoolean(record.tracked_reply),
scheduled_email_id: toStringNumberOrNull(record.scheduled_email_id),
campaign_id: toStringNumberOrNull(record.campaign_id),
lead_id: toNullableNumber(record.lead_id),
sender_email_id: toNullableNumber(record.sender_email_id),
raw_message_id: toStringOrNull(record.raw_message_id),
from_name: toStringOrNull(record.from_name),
from_email_address: toStringOrNull(record.from_email_address),
primary_to_email_address: toStringOrNull(record.primary_to_email_address),
to: toArray(record.to).map(mapReplyAddress),
cc: toStringOrNull(record.cc),
bcc: toStringOrNull(record.bcc),
parent_id: toStringNumberOrNull(record.parent_id),
attachments: toArray(record.attachments).map(mapReplyAttachment),
created_at: toStringOrNull(record.created_at),
updated_at: toStringOrNull(record.updated_at),
}
}
export function actionOutput(value: unknown): { success: boolean; message: string | null } {
const record = toRecord(value)
return {
success: record.success === true,
message: toStringOrNull(record.message),
}
}
export const leadOutputs = {
id: { type: 'number', description: 'Lead ID' },
first_name: { type: 'string', description: 'Lead first name' },
last_name: { type: 'string', description: 'Lead last name' },
email: { type: 'string', description: 'Lead email address' },
title: { type: 'string', description: 'Lead title', optional: true },
company: { type: 'string', description: 'Lead company', optional: true },
notes: { type: 'string', description: 'Lead notes', optional: true },
status: { type: 'string', description: 'Lead status', optional: true },
custom_variables: {
type: 'array',
description: 'Lead custom variables',
items: {
type: 'object',
properties: {
name: { type: 'string', description: 'Custom variable name' },
value: { type: 'string', description: 'Custom variable value', optional: true },
},
},
},
lead_campaign_data: {
type: 'array',
description: 'Lead campaign data returned by Email Bison',
},
overall_stats: {
type: 'object',
description: 'Lead engagement stats',
properties: {
emails_sent: { type: 'number', description: 'Emails sent' },
opens: { type: 'number', description: 'Email opens' },
replies: { type: 'number', description: 'Replies' },
unique_replies: { type: 'number', description: 'Unique replies' },
unique_opens: { type: 'number', description: 'Unique opens' },
},
},
created_at: { type: 'string', description: 'Lead creation timestamp', optional: true },
updated_at: { type: 'string', description: 'Lead update timestamp', optional: true },
} satisfies NonNullable<ToolConfig['outputs']>
const tagProperties = {
id: { type: 'number', description: 'Tag ID' },
name: { type: 'string', description: 'Tag name' },
default: { type: 'boolean', description: 'Whether this is a default tag' },
created_at: { type: 'string', description: 'Tag creation timestamp', optional: true },
updated_at: { type: 'string', description: 'Tag update timestamp', optional: true },
} satisfies Record<string, OutputProperty>
const campaignTagProperties = {
id: { type: 'number', description: 'Tag ID' },
name: { type: 'string', description: 'Tag name' },
default: { type: 'boolean', description: 'Whether this is a default tag' },
} satisfies Record<string, OutputProperty>
export const listLeadsOutputs = {
leads: {
type: 'array',
description: 'List of leads',
items: {
type: 'object',
properties: leadOutputs,
},
},
count: { type: 'number', description: 'Number of leads returned' },
} satisfies NonNullable<ToolConfig['outputs']>
export const campaignOutputs = {
id: { type: 'number', description: 'Campaign ID' },
uuid: { type: 'string', description: 'Campaign UUID', optional: true },
name: { type: 'string', description: 'Campaign name' },
type: { type: 'string', description: 'Campaign type', optional: true },
status: { type: 'string', description: 'Campaign status', optional: true },
emails_sent: { type: 'number', description: 'Emails sent' },
opened: { type: 'number', description: 'Total opens' },
unique_opens: { type: 'number', description: 'Unique opens' },
replied: { type: 'number', description: 'Total replies' },
unique_replies: { type: 'number', description: 'Unique replies' },
bounced: { type: 'number', description: 'Bounces' },
unsubscribed: { type: 'number', description: 'Unsubscribes' },
interested: { type: 'number', description: 'Interested replies' },
total_leads_contacted: { type: 'number', description: 'Total leads contacted' },
total_leads: { type: 'number', description: 'Total leads' },
max_emails_per_day: { type: 'number', description: 'Maximum emails per day', optional: true },
max_new_leads_per_day: {
type: 'number',
description: 'Maximum new leads per day',
optional: true,
},
plain_text: {
type: 'boolean',
description: 'Whether campaign emails are plain text',
optional: true,
},
open_tracking: {
type: 'boolean',
description: 'Whether open tracking is enabled',
optional: true,
},
can_unsubscribe: {
type: 'boolean',
description: 'Whether recipients can unsubscribe',
optional: true,
},
unsubscribe_text: { type: 'string', description: 'Unsubscribe text', optional: true },
tags: {
type: 'array',
description: 'Campaign tags',
items: { type: 'object', properties: campaignTagProperties },
},
created_at: { type: 'string', description: 'Campaign creation timestamp', optional: true },
updated_at: { type: 'string', description: 'Campaign update timestamp', optional: true },
} satisfies NonNullable<ToolConfig['outputs']>
export const listCampaignsOutputs = {
campaigns: {
type: 'array',
description: 'List of campaigns',
items: {
type: 'object',
properties: campaignOutputs,
},
},
count: { type: 'number', description: 'Number of campaigns returned' },
} satisfies NonNullable<ToolConfig['outputs']>
export const actionOutputs = {
success: { type: 'boolean', description: 'Whether the action succeeded' },
message: { type: 'string', description: 'Action message', optional: true },
} satisfies NonNullable<ToolConfig['outputs']>
export const listRepliesOutputs = {
replies: {
type: 'array',
description: 'List of replies',
items: {
type: 'object',
properties: {
id: { type: 'number', description: 'Reply ID' },
subject: { type: 'string', description: 'Reply subject', optional: true },
text_body: { type: 'string', description: 'Reply text body', optional: true },
from_email_address: { type: 'string', description: 'Sender email', optional: true },
primary_to_email_address: {
type: 'string',
description: 'Primary recipient',
optional: true,
},
date_received: { type: 'string', description: 'Date received', optional: true },
interested: { type: 'boolean', description: 'Whether the reply is marked interested' },
read: { type: 'boolean', description: 'Whether the reply is read' },
},
},
},
count: { type: 'number', description: 'Number of replies returned' },
} satisfies NonNullable<ToolConfig['outputs']>
export const tagOutputs = {
id: { type: 'number', description: 'Tag ID' },
name: { type: 'string', description: 'Tag name' },
default: { type: 'boolean', description: 'Whether this is a default tag' },
created_at: { type: 'string', description: 'Tag creation timestamp', optional: true },
updated_at: { type: 'string', description: 'Tag update timestamp', optional: true },
} satisfies NonNullable<ToolConfig['outputs']>
export const listTagsOutputs = {
tags: {
type: 'array',
description: 'List of tags',
items: {
type: 'object',
properties: tagProperties,
},
},
count: { type: 'number', description: 'Number of tags returned' },
} satisfies NonNullable<ToolConfig['outputs']>
function mapLeadStats(record: Record<string, unknown>): EmailBisonLeadStats {
return {
emails_sent: toNullableNumber(record.emails_sent),
opens: toNullableNumber(record.opens),
replies: toNullableNumber(record.replies),
unique_replies: toNullableNumber(record.unique_replies),
unique_opens: toNullableNumber(record.unique_opens),
}
}
function mapReplyAddress(value: unknown): EmailBisonReplyAddress {
const record = toRecord(value)
return {
name: toStringOrNull(record.name),
address: toStringOrNull(record.address),
}
}
function mapReplyAttachment(value: unknown): EmailBisonReplyAttachment {
const record = toRecord(value)
return {
id: toNullableNumber(record.id),
uuid: toStringOrNull(record.uuid),
reply_id: toNullableNumber(record.reply_id),
file_name: toStringOrNull(record.file_name),
download_url: toStringOrNull(record.download_url),
created_at: toStringOrNull(record.created_at),
updated_at: toStringOrNull(record.updated_at),
}
}
function toRecord(value: unknown): Record<string, unknown> {
return isRecordLike(value) ? value : {}
}
function toArray(value: unknown): unknown[] {
return Array.isArray(value) ? value : []
}
function toStringOrNull(value: unknown): string | null {
if (value === undefined || value === null) return null
return String(value)
}
function toStringNumberOrNull(value: unknown): number | string | null {
if (typeof value === 'number' || typeof value === 'string') return value
return null
}
function toNumber(value: unknown): number | null {
if (typeof value === 'number') return value
if (typeof value === 'string') {
const parsed = Number(value)
return Number.isFinite(parsed) ? parsed : null
}
return null
}
function toNullableNumber(value: unknown): number | null {
if (value === undefined || value === null) return null
return toNumber(value)
}
function toNullableBoolean(value: unknown): boolean | null {
if (value === undefined || value === null) return null
return typeof value === 'boolean' ? value : null
}