diff --git a/apps/docs/content/docs/en/tools/resend.mdx b/apps/docs/content/docs/en/tools/resend.mdx index 8ddc4e933d..be7f6bfdf1 100644 --- a/apps/docs/content/docs/en/tools/resend.mdx +++ b/apps/docs/content/docs/en/tools/resend.mdx @@ -1,6 +1,6 @@ --- title: Resend -description: Send emails with Resend. +description: Send emails and manage contacts with Resend. --- import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -27,7 +27,7 @@ In Sim, the Resend integration allows your agents to programmatically send email ## Usage Instructions -Integrate Resend into the workflow. Can send emails. Requires API Key. +Integrate Resend into your workflow. Send emails, retrieve email status, manage contacts, and view domains. Requires API Key. @@ -46,6 +46,11 @@ Send an email using your own Resend API key and from address | `subject` | string | Yes | Email subject line | | `body` | string | Yes | Email body content \(plain text or HTML based on contentType\) | | `contentType` | string | No | Content type for the email body: "text" for plain text or "html" for HTML content | +| `cc` | string | No | Carbon copy recipient email address | +| `bcc` | string | No | Blind carbon copy recipient email address | +| `replyTo` | string | No | Reply-to email address | +| `scheduledAt` | string | No | Schedule email to be sent later in ISO 8601 format | +| `tags` | string | No | Comma-separated key:value pairs for email tags \(e.g., "category:welcome,type:onboarding"\) | | `resendApiKey` | string | Yes | Resend API key for sending emails | #### Output @@ -53,8 +58,152 @@ Send an email using your own Resend API key and from address | Parameter | Type | Description | | --------- | ---- | ----------- | | `success` | boolean | Whether the email was sent successfully | +| `id` | string | Email ID returned by Resend | | `to` | string | Recipient email address | | `subject` | string | Email subject | | `body` | string | Email body content | +### `resend_get_email` + +Retrieve details of a previously sent email by its ID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `emailId` | string | Yes | The ID of the email to retrieve | +| `resendApiKey` | string | Yes | Resend API key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Email ID | +| `from` | string | Sender email address | +| `to` | json | Recipient email addresses | +| `subject` | string | Email subject | +| `html` | string | HTML email content | +| `text` | string | Plain text email content | +| `cc` | json | CC email addresses | +| `bcc` | json | BCC email addresses | +| `replyTo` | json | Reply-to email addresses | +| `lastEvent` | string | Last event status \(e.g., delivered, bounced\) | +| `createdAt` | string | Email creation timestamp | +| `scheduledAt` | string | Scheduled send timestamp | +| `tags` | json | Email tags as name-value pairs | + +### `resend_create_contact` + +Create a new contact in Resend + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `email` | string | Yes | Email address of the contact | +| `firstName` | string | No | First name of the contact | +| `lastName` | string | No | Last name of the contact | +| `unsubscribed` | boolean | No | Whether the contact is unsubscribed from all broadcasts | +| `resendApiKey` | string | Yes | Resend API key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Created contact ID | + +### `resend_list_contacts` + +List all contacts in Resend + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `resendApiKey` | string | Yes | Resend API key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `contacts` | json | Array of contacts with id, email, first_name, last_name, created_at, unsubscribed | +| `hasMore` | boolean | Whether there are more contacts to retrieve | + +### `resend_get_contact` + +Retrieve details of a contact by ID or email + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `contactId` | string | Yes | The contact ID or email address to retrieve | +| `resendApiKey` | string | Yes | Resend API key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Contact ID | +| `email` | string | Contact email address | +| `firstName` | string | Contact first name | +| `lastName` | string | Contact last name | +| `createdAt` | string | Contact creation timestamp | +| `unsubscribed` | boolean | Whether the contact is unsubscribed | + +### `resend_update_contact` + +Update an existing contact in Resend + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `contactId` | string | Yes | The contact ID or email address to update | +| `firstName` | string | No | Updated first name | +| `lastName` | string | No | Updated last name | +| `unsubscribed` | boolean | No | Whether the contact should be unsubscribed from all broadcasts | +| `resendApiKey` | string | Yes | Resend API key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Updated contact ID | + +### `resend_delete_contact` + +Delete a contact from Resend by ID or email + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `contactId` | string | Yes | The contact ID or email address to delete | +| `resendApiKey` | string | Yes | Resend API key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Deleted contact ID | +| `deleted` | boolean | Whether the contact was successfully deleted | + +### `resend_list_domains` + +List all verified domains in your Resend account + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `resendApiKey` | string | Yes | Resend API key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `domains` | json | Array of domains with id, name, status, region, and createdAt | +| `hasMore` | boolean | Whether there are more domains to retrieve | + diff --git a/apps/sim/app/api/tools/mail/send/route.ts b/apps/sim/app/api/tools/mail/send/route.ts index dbd37d50f0..b75f5c06eb 100644 --- a/apps/sim/app/api/tools/mail/send/route.ts +++ b/apps/sim/app/api/tools/mail/send/route.ts @@ -10,12 +10,26 @@ export const dynamic = 'force-dynamic' const logger = createLogger('MailSendAPI') const MailSendSchema = z.object({ - fromAddress: z.string().email('Invalid from email address').min(1, 'From address is required'), - to: z.string().email('Invalid email address').min(1, 'To email is required'), + fromAddress: z.string().min(1, 'From address is required'), + to: z.string().min(1, 'To email is required'), subject: z.string().min(1, 'Subject is required'), body: z.string().min(1, 'Email body is required'), contentType: z.enum(['text', 'html']).optional().nullable(), resendApiKey: z.string().min(1, 'Resend API key is required'), + cc: z + .union([z.string().min(1), z.array(z.string().min(1))]) + .optional() + .nullable(), + bcc: z + .union([z.string().min(1), z.array(z.string().min(1))]) + .optional() + .nullable(), + replyTo: z + .union([z.string().min(1), z.array(z.string().min(1))]) + .optional() + .nullable(), + scheduledAt: z.string().datetime().optional().nullable(), + tags: z.string().optional().nullable(), }) export async function POST(request: NextRequest) { @@ -52,23 +66,52 @@ export async function POST(request: NextRequest) { const resend = new Resend(validatedData.resendApiKey) const contentType = validatedData.contentType || 'text' - const emailData = - contentType === 'html' - ? { - from: validatedData.fromAddress, - to: validatedData.to, - subject: validatedData.subject, - html: validatedData.body, - text: validatedData.body.replace(/<[^>]*>/g, ''), // Strip HTML for text version - } - : { - from: validatedData.fromAddress, - to: validatedData.to, - subject: validatedData.subject, - text: validatedData.body, - } - - const { data, error } = await resend.emails.send(emailData) + const emailData: Record = { + from: validatedData.fromAddress, + to: validatedData.to, + subject: validatedData.subject, + } + + if (contentType === 'html') { + emailData.html = validatedData.body + emailData.text = validatedData.body.replace(/<[^>]*>/g, '') + } else { + emailData.text = validatedData.body + } + + if (validatedData.cc) { + emailData.cc = validatedData.cc + } + + if (validatedData.bcc) { + emailData.bcc = validatedData.bcc + } + + if (validatedData.replyTo) { + emailData.replyTo = validatedData.replyTo + } + + if (validatedData.scheduledAt) { + emailData.scheduledAt = validatedData.scheduledAt + } + + if (validatedData.tags) { + const tagPairs = validatedData.tags.split(',').map((pair) => { + const trimmed = pair.trim() + const colonIndex = trimmed.indexOf(':') + if (colonIndex === -1) return null + const name = trimmed.substring(0, colonIndex).trim() + const value = trimmed.substring(colonIndex + 1).trim() + return { name, value: value || '' } + }) + emailData.tags = tagPairs.filter( + (tag): tag is { name: string; value: string } => tag !== null && !!tag.name + ) + } + + const { data, error } = await resend.emails.send( + emailData as unknown as Parameters[0] + ) if (error) { logger.error(`[${requestId}] Email sending failed:`, error) diff --git a/apps/sim/blocks/blocks/resend.ts b/apps/sim/blocks/blocks/resend.ts index 909d5b20d4..eaf83943bd 100644 --- a/apps/sim/blocks/blocks/resend.ts +++ b/apps/sim/blocks/blocks/resend.ts @@ -1,23 +1,53 @@ import { ResendIcon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' -import type { MailSendResult } from '@/tools/resend/types' -export const ResendBlock: BlockConfig = { +export const ResendBlock: BlockConfig = { type: 'resend', name: 'Resend', - description: 'Send emails with Resend.', - longDescription: 'Integrate Resend into the workflow. Can send emails. Requires API Key.', + description: 'Send emails and manage contacts with Resend.', + longDescription: + 'Integrate Resend into your workflow. Send emails, retrieve email status, manage contacts, and view domains. Requires API Key.', docsLink: 'https://docs.sim.ai/tools/resend', category: 'tools', bgColor: '#181C1E', icon: ResendIcon, subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + // Email Operations + { label: 'Send Email', id: 'send_email' }, + { label: 'Get Email', id: 'get_email' }, + // Contact Operations + { label: 'Create Contact', id: 'create_contact' }, + { label: 'List Contacts', id: 'list_contacts' }, + { label: 'Get Contact', id: 'get_contact' }, + { label: 'Update Contact', id: 'update_contact' }, + { label: 'Delete Contact', id: 'delete_contact' }, + // Domain Operations + { label: 'List Domains', id: 'list_domains' }, + ], + value: () => 'send_email', + }, + { + id: 'resendApiKey', + title: 'Resend API Key', + type: 'short-input', + placeholder: 'Your Resend API key', + required: true, + password: true, + }, + + // Send Email fields { id: 'fromAddress', title: 'From Address', type: 'short-input', placeholder: 'sender@yourdomain.com', + condition: { field: 'operation', value: 'send_email' }, required: true, }, { @@ -25,6 +55,7 @@ export const ResendBlock: BlockConfig = { title: 'To', type: 'short-input', placeholder: 'recipient@example.com', + condition: { field: 'operation', value: 'send_email' }, required: true, }, { @@ -32,14 +63,46 @@ export const ResendBlock: BlockConfig = { title: 'Subject', type: 'short-input', placeholder: 'Email subject', + condition: { field: 'operation', value: 'send_email' }, required: true, + wandConfig: { + enabled: true, + prompt: `Generate a compelling email subject line based on the user's description. + +### GUIDELINES +- Keep it concise (50 characters or less is ideal) +- Make it attention-grabbing +- Avoid spam trigger words +- Be clear about the email content + +### EXAMPLES +"Welcome email for new users" -> "Welcome to Our Platform!" +"Order confirmation" -> "Your Order #12345 is Confirmed" +"Newsletter about new features" -> "New Features You'll Love" + +Return ONLY the subject line - no explanations.`, + placeholder: 'Describe the email topic...', + }, }, { id: 'body', title: 'Body', type: 'long-input', placeholder: 'Email body content', + condition: { field: 'operation', value: 'send_email' }, required: true, + wandConfig: { + enabled: true, + prompt: `Generate email content based on the user's description. + +### GUIDELINES +- Use clear, readable formatting +- Keep paragraphs short +- Include appropriate greeting and sign-off + +Return ONLY the email body - no explanations.`, + placeholder: 'Describe the email content...', + }, }, { id: 'contentType', @@ -50,45 +113,175 @@ export const ResendBlock: BlockConfig = { { label: 'HTML', id: 'html' }, ], value: () => 'text', - required: false, + condition: { field: 'operation', value: 'send_email' }, }, { - id: 'resendApiKey', - title: 'Resend API Key', + id: 'cc', + title: 'CC', type: 'short-input', - placeholder: 'Your Resend API key', + placeholder: 'cc@example.com', + condition: { field: 'operation', value: 'send_email' }, + }, + { + id: 'bcc', + title: 'BCC', + type: 'short-input', + placeholder: 'bcc@example.com', + condition: { field: 'operation', value: 'send_email' }, + }, + { + id: 'replyTo', + title: 'Reply To', + type: 'short-input', + placeholder: 'reply@example.com', + condition: { field: 'operation', value: 'send_email' }, + }, + { + id: 'scheduledAt', + title: 'Schedule At', + type: 'short-input', + placeholder: '2024-08-05T11:52:01.858Z', + condition: { field: 'operation', value: 'send_email' }, + }, + { + id: 'tags', + title: 'Tags', + type: 'short-input', + placeholder: 'category:welcome,type:onboarding', + condition: { field: 'operation', value: 'send_email' }, + }, + + // Get Email fields + { + id: 'emailId', + title: 'Email ID', + type: 'short-input', + placeholder: 'Email ID to retrieve', + condition: { field: 'operation', value: 'get_email' }, + required: true, + }, + + // Create Contact fields + { + id: 'email', + title: 'Email', + type: 'short-input', + placeholder: 'contact@example.com', + condition: { field: 'operation', value: 'create_contact' }, + required: true, + }, + { + id: 'firstName', + title: 'First Name', + type: 'short-input', + placeholder: 'John', + condition: { field: 'operation', value: ['create_contact', 'update_contact'] }, + }, + { + id: 'lastName', + title: 'Last Name', + type: 'short-input', + placeholder: 'Doe', + condition: { field: 'operation', value: ['create_contact', 'update_contact'] }, + }, + { + id: 'unsubscribed', + title: 'Unsubscribed', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: ['create_contact', 'update_contact'] }, + }, + + // Get/Update/Delete Contact fields + { + id: 'contactId', + title: 'Contact ID or Email', + type: 'short-input', + placeholder: 'Contact ID or email address', + condition: { field: 'operation', value: ['get_contact', 'update_contact', 'delete_contact'] }, required: true, - password: true, }, ], tools: { - access: ['resend_send'], + access: [ + 'resend_send', + 'resend_get_email', + 'resend_create_contact', + 'resend_list_contacts', + 'resend_get_contact', + 'resend_update_contact', + 'resend_delete_contact', + 'resend_list_domains', + ], config: { - tool: () => 'resend_send', - params: (params) => ({ - resendApiKey: params.resendApiKey, - fromAddress: params.fromAddress, - to: params.to, - subject: params.subject, - body: params.body, - }), + tool: (params) => { + const operation = params.operation || 'send_email' + if (operation === 'send_email') return 'resend_send' + return `resend_${operation}` + }, + params: (params) => { + const { operation, ...rest } = params + + if (rest.unsubscribed !== undefined) { + rest.unsubscribed = rest.unsubscribed === 'true' + } + + return rest + }, }, }, inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + resendApiKey: { type: 'string', description: 'Resend API key' }, + // Send email inputs fromAddress: { type: 'string', description: 'Email address to send from' }, to: { type: 'string', description: 'Recipient email address' }, subject: { type: 'string', description: 'Email subject' }, body: { type: 'string', description: 'Email body content' }, contentType: { type: 'string', description: 'Content type (text or html)' }, - resendApiKey: { type: 'string', description: 'Resend API key for sending emails' }, + cc: { type: 'string', description: 'CC email address' }, + bcc: { type: 'string', description: 'BCC email address' }, + replyTo: { type: 'string', description: 'Reply-to email address' }, + scheduledAt: { type: 'string', description: 'Scheduled send time in ISO 8601 format' }, + tags: { type: 'string', description: 'Email tags as key:value pairs' }, + // Get email inputs + emailId: { type: 'string', description: 'Email ID to retrieve' }, + // Contact inputs + email: { type: 'string', description: 'Contact email address' }, + firstName: { type: 'string', description: 'Contact first name' }, + lastName: { type: 'string', description: 'Contact last name' }, + unsubscribed: { type: 'string', description: 'Contact subscription status' }, + contactId: { type: 'string', description: 'Contact ID or email address' }, }, outputs: { - success: { type: 'boolean', description: 'Whether the email was sent successfully' }, + success: { type: 'boolean', description: 'Operation success status' }, + // Send email outputs to: { type: 'string', description: 'Recipient email address' }, subject: { type: 'string', description: 'Email subject' }, body: { type: 'string', description: 'Email body content' }, + // Get email outputs + id: { type: 'string', description: 'Email or contact ID' }, + from: { type: 'string', description: 'Sender email address' }, + html: { type: 'string', description: 'HTML email content' }, + text: { type: 'string', description: 'Plain text email content' }, + lastEvent: { type: 'string', description: 'Last event status' }, + createdAt: { type: 'string', description: 'Creation timestamp' }, + tags: { type: 'json', description: 'Email tags as name-value pairs' }, + // Contact outputs + email: { type: 'string', description: 'Contact email address' }, + firstName: { type: 'string', description: 'Contact first name' }, + lastName: { type: 'string', description: 'Contact last name' }, + contacts: { type: 'json', description: 'Array of contacts' }, + // Domain outputs + domains: { type: 'json', description: 'Array of domains' }, + hasMore: { type: 'boolean', description: 'Whether more results are available' }, + deleted: { type: 'boolean', description: 'Whether the resource was deleted' }, }, } diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 6ad7a864d9..89ac47eedc 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -1578,7 +1578,16 @@ import { redisTtlTool, } from '@/tools/redis' import { reductoParserTool, reductoParserV2Tool } from '@/tools/reducto' -import { mailSendTool } from '@/tools/resend' +import { + mailSendTool, + resendCreateContactTool, + resendDeleteContactTool, + resendGetContactTool, + resendGetEmailTool, + resendListContactsTool, + resendListDomainsTool, + resendUpdateContactTool, +} from '@/tools/resend' import { revenuecatCreatePurchaseTool, revenuecatDeferGoogleSubscriptionTool, @@ -2319,6 +2328,13 @@ export const tools: Record = { linkedin_share_post: linkedInSharePostTool, linkedin_get_profile: linkedInGetProfileTool, resend_send: mailSendTool, + resend_get_email: resendGetEmailTool, + resend_create_contact: resendCreateContactTool, + resend_list_contacts: resendListContactsTool, + resend_get_contact: resendGetContactTool, + resend_update_contact: resendUpdateContactTool, + resend_delete_contact: resendDeleteContactTool, + resend_list_domains: resendListDomainsTool, sendgrid_send_mail: sendGridSendMailTool, sendgrid_add_contact: sendGridAddContactTool, sendgrid_get_contact: sendGridGetContactTool, diff --git a/apps/sim/tools/resend/create_contact.ts b/apps/sim/tools/resend/create_contact.ts new file mode 100644 index 0000000000..e350765753 --- /dev/null +++ b/apps/sim/tools/resend/create_contact.ts @@ -0,0 +1,72 @@ +import type { CreateContactParams, CreateContactResult } from '@/tools/resend/types' +import type { ToolConfig } from '@/tools/types' + +export const resendCreateContactTool: ToolConfig = { + id: 'resend_create_contact', + name: 'Create Contact', + description: 'Create a new contact in Resend', + version: '1.0.0', + + params: { + email: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Email address of the contact', + }, + firstName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'First name of the contact', + }, + lastName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Last name of the contact', + }, + unsubscribed: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether the contact is unsubscribed from all broadcasts', + }, + resendApiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Resend API key', + }, + }, + + request: { + url: 'https://api.resend.com/contacts', + method: 'POST', + headers: (params: CreateContactParams) => ({ + Authorization: `Bearer ${params.resendApiKey}`, + 'Content-Type': 'application/json', + }), + body: (params: CreateContactParams) => ({ + email: params.email, + ...(params.firstName && { first_name: params.firstName }), + ...(params.lastName && { last_name: params.lastName }), + ...(params.unsubscribed !== undefined && { unsubscribed: params.unsubscribed }), + }), + }, + + transformResponse: async (response: Response): Promise => { + const data = await response.json() + + return { + success: true, + output: { + id: data.id, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Created contact ID' }, + }, +} diff --git a/apps/sim/tools/resend/delete_contact.ts b/apps/sim/tools/resend/delete_contact.ts new file mode 100644 index 0000000000..d9b1739a49 --- /dev/null +++ b/apps/sim/tools/resend/delete_contact.ts @@ -0,0 +1,51 @@ +import type { DeleteContactParams, DeleteContactResult } from '@/tools/resend/types' +import type { ToolConfig } from '@/tools/types' + +export const resendDeleteContactTool: ToolConfig = { + id: 'resend_delete_contact', + name: 'Delete Contact', + description: 'Delete a contact from Resend by ID or email', + version: '1.0.0', + + params: { + contactId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The contact ID or email address to delete', + }, + resendApiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Resend API key', + }, + }, + + request: { + url: (params: DeleteContactParams) => + `https://api.resend.com/contacts/${encodeURIComponent(params.contactId)}`, + method: 'DELETE', + headers: (params: DeleteContactParams) => ({ + Authorization: `Bearer ${params.resendApiKey}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response): Promise => { + const data = await response.json() + + return { + success: true, + output: { + id: data.contact || data.id || '', + deleted: data.deleted ?? true, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Deleted contact ID' }, + deleted: { type: 'boolean', description: 'Whether the contact was successfully deleted' }, + }, +} diff --git a/apps/sim/tools/resend/get_contact.ts b/apps/sim/tools/resend/get_contact.ts new file mode 100644 index 0000000000..c521a5e725 --- /dev/null +++ b/apps/sim/tools/resend/get_contact.ts @@ -0,0 +1,59 @@ +import type { GetContactParams, GetContactResult } from '@/tools/resend/types' +import type { ToolConfig } from '@/tools/types' + +export const resendGetContactTool: ToolConfig = { + id: 'resend_get_contact', + name: 'Get Contact', + description: 'Retrieve details of a contact by ID or email', + version: '1.0.0', + + params: { + contactId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The contact ID or email address to retrieve', + }, + resendApiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Resend API key', + }, + }, + + request: { + url: (params: GetContactParams) => + `https://api.resend.com/contacts/${encodeURIComponent(params.contactId)}`, + method: 'GET', + headers: (params: GetContactParams) => ({ + Authorization: `Bearer ${params.resendApiKey}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response): Promise => { + const data = await response.json() + + return { + success: true, + output: { + id: data.id, + email: data.email, + firstName: data.first_name || '', + lastName: data.last_name || '', + createdAt: data.created_at || '', + unsubscribed: data.unsubscribed || false, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Contact ID' }, + email: { type: 'string', description: 'Contact email address' }, + firstName: { type: 'string', description: 'Contact first name' }, + lastName: { type: 'string', description: 'Contact last name' }, + createdAt: { type: 'string', description: 'Contact creation timestamp' }, + unsubscribed: { type: 'boolean', description: 'Whether the contact is unsubscribed' }, + }, +} diff --git a/apps/sim/tools/resend/get_email.ts b/apps/sim/tools/resend/get_email.ts new file mode 100644 index 0000000000..4803b46467 --- /dev/null +++ b/apps/sim/tools/resend/get_email.ts @@ -0,0 +1,72 @@ +import type { GetEmailParams, GetEmailResult } from '@/tools/resend/types' +import type { ToolConfig } from '@/tools/types' + +export const resendGetEmailTool: ToolConfig = { + id: 'resend_get_email', + name: 'Get Email', + description: 'Retrieve details of a previously sent email by its ID', + version: '1.0.0', + + params: { + emailId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the email to retrieve', + }, + resendApiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Resend API key', + }, + }, + + request: { + url: (params: GetEmailParams) => `https://api.resend.com/emails/${params.emailId}`, + method: 'GET', + headers: (params: GetEmailParams) => ({ + Authorization: `Bearer ${params.resendApiKey}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response): Promise => { + const data = await response.json() + + return { + success: true, + output: { + id: data.id, + from: data.from, + to: data.to || [], + subject: data.subject, + html: data.html || '', + text: data.text || null, + cc: data.cc || [], + bcc: data.bcc || [], + replyTo: data.reply_to || [], + lastEvent: data.last_event || '', + createdAt: data.created_at || '', + scheduledAt: data.scheduled_at || null, + tags: data.tags || [], + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Email ID' }, + from: { type: 'string', description: 'Sender email address' }, + to: { type: 'json', description: 'Recipient email addresses' }, + subject: { type: 'string', description: 'Email subject' }, + html: { type: 'string', description: 'HTML email content' }, + text: { type: 'string', description: 'Plain text email content' }, + cc: { type: 'json', description: 'CC email addresses' }, + bcc: { type: 'json', description: 'BCC email addresses' }, + replyTo: { type: 'json', description: 'Reply-to email addresses' }, + lastEvent: { type: 'string', description: 'Last event status (e.g., delivered, bounced)' }, + createdAt: { type: 'string', description: 'Email creation timestamp' }, + scheduledAt: { type: 'string', description: 'Scheduled send timestamp' }, + tags: { type: 'json', description: 'Email tags as name-value pairs' }, + }, +} diff --git a/apps/sim/tools/resend/index.ts b/apps/sim/tools/resend/index.ts index b945a12b48..846b46be5a 100644 --- a/apps/sim/tools/resend/index.ts +++ b/apps/sim/tools/resend/index.ts @@ -1,3 +1,26 @@ -import { mailSendTool } from '@/tools/resend/send' - -export { mailSendTool } +export { resendCreateContactTool } from '@/tools/resend/create_contact' +export { resendDeleteContactTool } from '@/tools/resend/delete_contact' +export { resendGetContactTool } from '@/tools/resend/get_contact' +export { resendGetEmailTool } from '@/tools/resend/get_email' +export { resendListContactsTool } from '@/tools/resend/list_contacts' +export { resendListDomainsTool } from '@/tools/resend/list_domains' +export { mailSendTool } from '@/tools/resend/send' +export type { + CreateContactParams, + CreateContactResult, + DeleteContactParams, + DeleteContactResult, + GetContactParams, + GetContactResult, + GetEmailParams, + GetEmailResult, + ListContactsParams, + ListContactsResult, + ListDomainsParams, + ListDomainsResult, + MailSendParams, + MailSendResult, + UpdateContactParams, + UpdateContactResult, +} from '@/tools/resend/types' +export { resendUpdateContactTool } from '@/tools/resend/update_contact' diff --git a/apps/sim/tools/resend/list_contacts.ts b/apps/sim/tools/resend/list_contacts.ts new file mode 100644 index 0000000000..e3692beea8 --- /dev/null +++ b/apps/sim/tools/resend/list_contacts.ts @@ -0,0 +1,48 @@ +import type { ListContactsParams, ListContactsResult } from '@/tools/resend/types' +import type { ToolConfig } from '@/tools/types' + +export const resendListContactsTool: ToolConfig = { + id: 'resend_list_contacts', + name: 'List Contacts', + description: 'List all contacts in Resend', + version: '1.0.0', + + params: { + resendApiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Resend API key', + }, + }, + + request: { + url: 'https://api.resend.com/contacts', + method: 'GET', + headers: (params: ListContactsParams) => ({ + Authorization: `Bearer ${params.resendApiKey}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response): Promise => { + const data = await response.json() + + return { + success: true, + output: { + contacts: data.data || [], + hasMore: data.has_more || false, + }, + } + }, + + outputs: { + contacts: { + type: 'json', + description: + 'Array of contacts with id, email, first_name, last_name, created_at, unsubscribed', + }, + hasMore: { type: 'boolean', description: 'Whether there are more contacts to retrieve' }, + }, +} diff --git a/apps/sim/tools/resend/list_domains.ts b/apps/sim/tools/resend/list_domains.ts new file mode 100644 index 0000000000..f6fce0730a --- /dev/null +++ b/apps/sim/tools/resend/list_domains.ts @@ -0,0 +1,61 @@ +import type { ListDomainsParams, ListDomainsResult } from '@/tools/resend/types' +import type { ToolConfig } from '@/tools/types' + +export const resendListDomainsTool: ToolConfig = { + id: 'resend_list_domains', + name: 'List Domains', + description: 'List all verified domains in your Resend account', + version: '1.0.0', + + params: { + resendApiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Resend API key', + }, + }, + + request: { + url: 'https://api.resend.com/domains', + method: 'GET', + headers: (params: ListDomainsParams) => ({ + Authorization: `Bearer ${params.resendApiKey}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response): Promise => { + const data = await response.json() + + return { + success: true, + output: { + domains: (data.data || []).map( + (domain: { + id: string + name: string + status: string + region: string + created_at: string + }) => ({ + id: domain.id, + name: domain.name, + status: domain.status, + region: domain.region, + createdAt: domain.created_at, + }) + ), + hasMore: data.has_more || false, + }, + } + }, + + outputs: { + domains: { + type: 'json', + description: 'Array of domains with id, name, status, region, and createdAt', + }, + hasMore: { type: 'boolean', description: 'Whether there are more domains to retrieve' }, + }, +} diff --git a/apps/sim/tools/resend/send.ts b/apps/sim/tools/resend/send.ts index d70305c532..2ed69b92d7 100644 --- a/apps/sim/tools/resend/send.ts +++ b/apps/sim/tools/resend/send.ts @@ -41,6 +41,37 @@ export const mailSendTool: ToolConfig = { description: 'Content type for the email body: "text" for plain text or "html" for HTML content', }, + cc: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Carbon copy recipient email address', + }, + bcc: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Blind carbon copy recipient email address', + }, + replyTo: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Reply-to email address', + }, + scheduledAt: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Schedule email to be sent later in ISO 8601 format', + }, + tags: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Comma-separated key:value pairs for email tags (e.g., "category:welcome,type:onboarding")', + }, resendApiKey: { type: 'string', required: true, @@ -62,6 +93,11 @@ export const mailSendTool: ToolConfig = { subject: params.subject, body: params.body, contentType: params.contentType || 'text', + ...(params.cc && { cc: params.cc }), + ...(params.bcc && { bcc: params.bcc }), + ...(params.replyTo && { replyTo: params.replyTo }), + ...(params.scheduledAt && { scheduledAt: params.scheduledAt }), + ...(params.tags && { tags: params.tags }), }), }, @@ -72,6 +108,7 @@ export const mailSendTool: ToolConfig = { success: true, output: { success: result.success, + id: result.data?.id || '', to: params?.to || '', subject: params?.subject || '', body: params?.body || '', @@ -81,6 +118,7 @@ export const mailSendTool: ToolConfig = { outputs: { success: { type: 'boolean', description: 'Whether the email was sent successfully' }, + id: { type: 'string', description: 'Email ID returned by Resend' }, to: { type: 'string', description: 'Recipient email address' }, subject: { type: 'string', description: 'Email subject' }, body: { type: 'string', description: 'Email body content' }, diff --git a/apps/sim/tools/resend/types.ts b/apps/sim/tools/resend/types.ts index 67a2a03e10..52a013b832 100644 --- a/apps/sim/tools/resend/types.ts +++ b/apps/sim/tools/resend/types.ts @@ -1,5 +1,6 @@ import type { ToolResponse } from '@/tools/types' +/** Send Email */ export interface MailSendParams { resendApiKey: string fromAddress: string @@ -7,13 +8,140 @@ export interface MailSendParams { subject: string body: string contentType?: 'text' | 'html' + cc?: string + bcc?: string + replyTo?: string + scheduledAt?: string + tags?: string } export interface MailSendResult extends ToolResponse { output: { success: boolean + id: string to: string subject: string body: string } } + +/** Get Email */ +export interface GetEmailParams { + resendApiKey: string + emailId: string +} + +export interface GetEmailResult extends ToolResponse { + output: { + id: string + from: string + to: string[] + subject: string + html: string + text: string | null + cc: string[] + bcc: string[] + replyTo: string[] + lastEvent: string + createdAt: string + scheduledAt: string | null + tags: Array<{ name: string; value: string }> + } +} + +/** Create Contact */ +export interface CreateContactParams { + resendApiKey: string + email: string + firstName?: string + lastName?: string + unsubscribed?: boolean +} + +export interface CreateContactResult extends ToolResponse { + output: { + id: string + } +} + +/** List Contacts */ +export interface ListContactsParams { + resendApiKey: string +} + +export interface ListContactsResult extends ToolResponse { + output: { + contacts: Array<{ + id: string + email: string + first_name: string + last_name: string + created_at: string + unsubscribed: boolean + }> + hasMore: boolean + } +} + +/** Get Contact */ +export interface GetContactParams { + resendApiKey: string + contactId: string +} + +export interface GetContactResult extends ToolResponse { + output: { + id: string + email: string + firstName: string + lastName: string + createdAt: string + unsubscribed: boolean + } +} + +/** Update Contact */ +export interface UpdateContactParams { + resendApiKey: string + contactId: string + firstName?: string + lastName?: string + unsubscribed?: boolean +} + +export interface UpdateContactResult extends ToolResponse { + output: { + id: string + } +} + +/** Delete Contact */ +export interface DeleteContactParams { + resendApiKey: string + contactId: string +} + +export interface DeleteContactResult extends ToolResponse { + output: { + id: string + deleted: boolean + } +} + +/** List Domains */ +export interface ListDomainsParams { + resendApiKey: string +} + +export interface ListDomainsResult extends ToolResponse { + output: { + domains: Array<{ + id: string + name: string + status: string + region: string + createdAt: string + }> + hasMore: boolean + } +} diff --git a/apps/sim/tools/resend/update_contact.ts b/apps/sim/tools/resend/update_contact.ts new file mode 100644 index 0000000000..59d7bfb2cf --- /dev/null +++ b/apps/sim/tools/resend/update_contact.ts @@ -0,0 +1,72 @@ +import type { UpdateContactParams, UpdateContactResult } from '@/tools/resend/types' +import type { ToolConfig } from '@/tools/types' + +export const resendUpdateContactTool: ToolConfig = { + id: 'resend_update_contact', + name: 'Update Contact', + description: 'Update an existing contact in Resend', + version: '1.0.0', + + params: { + contactId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The contact ID or email address to update', + }, + firstName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Updated first name', + }, + lastName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Updated last name', + }, + unsubscribed: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether the contact should be unsubscribed from all broadcasts', + }, + resendApiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Resend API key', + }, + }, + + request: { + url: (params: UpdateContactParams) => + `https://api.resend.com/contacts/${encodeURIComponent(params.contactId)}`, + method: 'PATCH', + headers: (params: UpdateContactParams) => ({ + Authorization: `Bearer ${params.resendApiKey}`, + 'Content-Type': 'application/json', + }), + body: (params: UpdateContactParams) => ({ + ...(params.firstName !== undefined && { first_name: params.firstName }), + ...(params.lastName !== undefined && { last_name: params.lastName }), + ...(params.unsubscribed !== undefined && { unsubscribed: params.unsubscribed }), + }), + }, + + transformResponse: async (response: Response): Promise => { + const data = await response.json() + + return { + success: true, + output: { + id: data.id, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Updated contact ID' }, + }, +}