Skip to content

Commit 3b3913b

Browse files
committed
feat(resend): expand integration with contacts, domains, and enhanced email ops
1 parent 4ccb573 commit 3b3913b

File tree

13 files changed

+908
-41
lines changed

13 files changed

+908
-41
lines changed

apps/sim/app/api/tools/mail/send/route.ts

Lines changed: 58 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,20 @@ const MailSendSchema = z.object({
1616
body: z.string().min(1, 'Email body is required'),
1717
contentType: z.enum(['text', 'html']).optional().nullable(),
1818
resendApiKey: z.string().min(1, 'Resend API key is required'),
19+
cc: z
20+
.union([z.string().email(), z.array(z.string().email())])
21+
.optional()
22+
.nullable(),
23+
bcc: z
24+
.union([z.string().email(), z.array(z.string().email())])
25+
.optional()
26+
.nullable(),
27+
replyTo: z
28+
.union([z.string().email(), z.array(z.string().email())])
29+
.optional()
30+
.nullable(),
31+
scheduledAt: z.string().datetime().optional().nullable(),
32+
tags: z.string().optional().nullable(),
1933
})
2034

2135
export async function POST(request: NextRequest) {
@@ -52,23 +66,50 @@ export async function POST(request: NextRequest) {
5266
const resend = new Resend(validatedData.resendApiKey)
5367

5468
const contentType = validatedData.contentType || 'text'
55-
const emailData =
56-
contentType === 'html'
57-
? {
58-
from: validatedData.fromAddress,
59-
to: validatedData.to,
60-
subject: validatedData.subject,
61-
html: validatedData.body,
62-
text: validatedData.body.replace(/<[^>]*>/g, ''), // Strip HTML for text version
63-
}
64-
: {
65-
from: validatedData.fromAddress,
66-
to: validatedData.to,
67-
subject: validatedData.subject,
68-
text: validatedData.body,
69-
}
70-
71-
const { data, error } = await resend.emails.send(emailData)
69+
const emailData: Record<string, unknown> = {
70+
from: validatedData.fromAddress,
71+
to: validatedData.to,
72+
subject: validatedData.subject,
73+
}
74+
75+
if (contentType === 'html') {
76+
emailData.html = validatedData.body
77+
emailData.text = validatedData.body.replace(/<[^>]*>/g, '')
78+
} else {
79+
emailData.text = validatedData.body
80+
}
81+
82+
if (validatedData.cc) {
83+
emailData.cc = validatedData.cc
84+
}
85+
86+
if (validatedData.bcc) {
87+
emailData.bcc = validatedData.bcc
88+
}
89+
90+
if (validatedData.replyTo) {
91+
emailData.replyTo = validatedData.replyTo
92+
}
93+
94+
if (validatedData.scheduledAt) {
95+
emailData.scheduledAt = validatedData.scheduledAt
96+
}
97+
98+
if (validatedData.tags) {
99+
const tagPairs = validatedData.tags.split(',').map((pair) => {
100+
const trimmed = pair.trim()
101+
if (!trimmed.includes(':')) return null
102+
const [name, value] = trimmed.split(':')
103+
return { name: name?.trim() || '', value: value?.trim() || '' }
104+
})
105+
emailData.tags = tagPairs.filter(
106+
(tag): tag is { name: string; value: string } => tag !== null && !!tag.name
107+
)
108+
}
109+
110+
const { data, error } = await resend.emails.send(
111+
emailData as Parameters<typeof resend.emails.send>[0]
112+
)
72113

73114
if (error) {
74115
logger.error(`[${requestId}] Email sending failed:`, error)

apps/sim/blocks/blocks/resend.ts

Lines changed: 212 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,108 @@
11
import { ResendIcon } from '@/components/icons'
22
import type { BlockConfig } from '@/blocks/types'
3-
import type { MailSendResult } from '@/tools/resend/types'
43

5-
export const ResendBlock: BlockConfig<MailSendResult> = {
4+
export const ResendBlock: BlockConfig = {
65
type: 'resend',
76
name: 'Resend',
8-
description: 'Send emails with Resend.',
9-
longDescription: 'Integrate Resend into the workflow. Can send emails. Requires API Key.',
7+
description: 'Send emails and manage contacts with Resend.',
8+
longDescription:
9+
'Integrate Resend into your workflow. Send emails, retrieve email status, manage contacts, and view domains. Requires API Key.',
1010
docsLink: 'https://docs.sim.ai/tools/resend',
1111
category: 'tools',
1212
bgColor: '#181C1E',
1313
icon: ResendIcon,
1414

1515
subBlocks: [
16+
{
17+
id: 'operation',
18+
title: 'Operation',
19+
type: 'dropdown',
20+
options: [
21+
// Email Operations
22+
{ label: 'Send Email', id: 'send_email' },
23+
{ label: 'Get Email', id: 'get_email' },
24+
// Contact Operations
25+
{ label: 'Create Contact', id: 'create_contact' },
26+
{ label: 'List Contacts', id: 'list_contacts' },
27+
{ label: 'Get Contact', id: 'get_contact' },
28+
{ label: 'Update Contact', id: 'update_contact' },
29+
{ label: 'Delete Contact', id: 'delete_contact' },
30+
// Domain Operations
31+
{ label: 'List Domains', id: 'list_domains' },
32+
],
33+
value: () => 'send_email',
34+
},
35+
{
36+
id: 'resendApiKey',
37+
title: 'Resend API Key',
38+
type: 'short-input',
39+
placeholder: 'Your Resend API key',
40+
required: true,
41+
password: true,
42+
},
43+
44+
// Send Email fields
1645
{
1746
id: 'fromAddress',
1847
title: 'From Address',
1948
type: 'short-input',
2049
placeholder: 'sender@yourdomain.com',
50+
condition: { field: 'operation', value: 'send_email' },
2151
required: true,
2252
},
2353
{
2454
id: 'to',
2555
title: 'To',
2656
type: 'short-input',
2757
placeholder: 'recipient@example.com',
58+
condition: { field: 'operation', value: 'send_email' },
2859
required: true,
2960
},
3061
{
3162
id: 'subject',
3263
title: 'Subject',
3364
type: 'short-input',
3465
placeholder: 'Email subject',
66+
condition: { field: 'operation', value: 'send_email' },
3567
required: true,
68+
wandConfig: {
69+
enabled: true,
70+
prompt: `Generate a compelling email subject line based on the user's description.
71+
72+
### GUIDELINES
73+
- Keep it concise (50 characters or less is ideal)
74+
- Make it attention-grabbing
75+
- Avoid spam trigger words
76+
- Be clear about the email content
77+
78+
### EXAMPLES
79+
"Welcome email for new users" -> "Welcome to Our Platform!"
80+
"Order confirmation" -> "Your Order #12345 is Confirmed"
81+
"Newsletter about new features" -> "New Features You'll Love"
82+
83+
Return ONLY the subject line - no explanations.`,
84+
placeholder: 'Describe the email topic...',
85+
},
3686
},
3787
{
3888
id: 'body',
3989
title: 'Body',
4090
type: 'long-input',
4191
placeholder: 'Email body content',
92+
condition: { field: 'operation', value: 'send_email' },
4293
required: true,
94+
wandConfig: {
95+
enabled: true,
96+
prompt: `Generate email content based on the user's description.
97+
98+
### GUIDELINES
99+
- Use clear, readable formatting
100+
- Keep paragraphs short
101+
- Include appropriate greeting and sign-off
102+
103+
Return ONLY the email body - no explanations.`,
104+
placeholder: 'Describe the email content...',
105+
},
43106
},
44107
{
45108
id: 'contentType',
@@ -50,45 +113,174 @@ export const ResendBlock: BlockConfig<MailSendResult> = {
50113
{ label: 'HTML', id: 'html' },
51114
],
52115
value: () => 'text',
53-
required: false,
116+
condition: { field: 'operation', value: 'send_email' },
54117
},
55118
{
56-
id: 'resendApiKey',
57-
title: 'Resend API Key',
119+
id: 'cc',
120+
title: 'CC',
58121
type: 'short-input',
59-
placeholder: 'Your Resend API key',
122+
placeholder: 'cc@example.com',
123+
condition: { field: 'operation', value: 'send_email' },
124+
},
125+
{
126+
id: 'bcc',
127+
title: 'BCC',
128+
type: 'short-input',
129+
placeholder: 'bcc@example.com',
130+
condition: { field: 'operation', value: 'send_email' },
131+
},
132+
{
133+
id: 'replyTo',
134+
title: 'Reply To',
135+
type: 'short-input',
136+
placeholder: 'reply@example.com',
137+
condition: { field: 'operation', value: 'send_email' },
138+
},
139+
{
140+
id: 'scheduledAt',
141+
title: 'Schedule At',
142+
type: 'short-input',
143+
placeholder: '2024-08-05T11:52:01.858Z',
144+
condition: { field: 'operation', value: 'send_email' },
145+
},
146+
{
147+
id: 'tags',
148+
title: 'Tags',
149+
type: 'short-input',
150+
placeholder: 'category:welcome,type:onboarding',
151+
condition: { field: 'operation', value: 'send_email' },
152+
},
153+
154+
// Get Email fields
155+
{
156+
id: 'emailId',
157+
title: 'Email ID',
158+
type: 'short-input',
159+
placeholder: 'Email ID to retrieve',
160+
condition: { field: 'operation', value: 'get_email' },
161+
required: true,
162+
},
163+
164+
// Create Contact fields
165+
{
166+
id: 'email',
167+
title: 'Email',
168+
type: 'short-input',
169+
placeholder: 'contact@example.com',
170+
condition: { field: 'operation', value: 'create_contact' },
171+
required: true,
172+
},
173+
{
174+
id: 'firstName',
175+
title: 'First Name',
176+
type: 'short-input',
177+
placeholder: 'John',
178+
condition: { field: 'operation', value: ['create_contact', 'update_contact'] },
179+
},
180+
{
181+
id: 'lastName',
182+
title: 'Last Name',
183+
type: 'short-input',
184+
placeholder: 'Doe',
185+
condition: { field: 'operation', value: ['create_contact', 'update_contact'] },
186+
},
187+
{
188+
id: 'unsubscribed',
189+
title: 'Unsubscribed',
190+
type: 'dropdown',
191+
options: [
192+
{ label: 'No', id: 'false' },
193+
{ label: 'Yes', id: 'true' },
194+
],
195+
value: () => 'false',
196+
condition: { field: 'operation', value: ['create_contact', 'update_contact'] },
197+
},
198+
199+
// Get/Update/Delete Contact fields
200+
{
201+
id: 'contactId',
202+
title: 'Contact ID or Email',
203+
type: 'short-input',
204+
placeholder: 'Contact ID or email address',
205+
condition: { field: 'operation', value: ['get_contact', 'update_contact', 'delete_contact'] },
60206
required: true,
61-
password: true,
62207
},
63208
],
64209

65210
tools: {
66-
access: ['resend_send'],
211+
access: [
212+
'resend_send',
213+
'resend_get_email',
214+
'resend_create_contact',
215+
'resend_list_contacts',
216+
'resend_get_contact',
217+
'resend_update_contact',
218+
'resend_delete_contact',
219+
'resend_list_domains',
220+
],
67221
config: {
68-
tool: () => 'resend_send',
69-
params: (params) => ({
70-
resendApiKey: params.resendApiKey,
71-
fromAddress: params.fromAddress,
72-
to: params.to,
73-
subject: params.subject,
74-
body: params.body,
75-
}),
222+
tool: (params) => {
223+
const operation = params.operation || 'send_email'
224+
if (operation === 'send_email') return 'resend_send'
225+
return `resend_${operation}`
226+
},
227+
params: (params) => {
228+
const { operation, ...rest } = params
229+
230+
if (rest.unsubscribed !== undefined) {
231+
rest.unsubscribed = rest.unsubscribed === 'true'
232+
}
233+
234+
return rest
235+
},
76236
},
77237
},
78238

79239
inputs: {
240+
operation: { type: 'string', description: 'Operation to perform' },
241+
resendApiKey: { type: 'string', description: 'Resend API key' },
242+
// Send email inputs
80243
fromAddress: { type: 'string', description: 'Email address to send from' },
81244
to: { type: 'string', description: 'Recipient email address' },
82245
subject: { type: 'string', description: 'Email subject' },
83246
body: { type: 'string', description: 'Email body content' },
84247
contentType: { type: 'string', description: 'Content type (text or html)' },
85-
resendApiKey: { type: 'string', description: 'Resend API key for sending emails' },
248+
cc: { type: 'string', description: 'CC email address' },
249+
bcc: { type: 'string', description: 'BCC email address' },
250+
replyTo: { type: 'string', description: 'Reply-to email address' },
251+
scheduledAt: { type: 'string', description: 'Scheduled send time in ISO 8601 format' },
252+
tags: { type: 'string', description: 'Email tags as key:value pairs' },
253+
// Get email inputs
254+
emailId: { type: 'string', description: 'Email ID to retrieve' },
255+
// Contact inputs
256+
email: { type: 'string', description: 'Contact email address' },
257+
firstName: { type: 'string', description: 'Contact first name' },
258+
lastName: { type: 'string', description: 'Contact last name' },
259+
unsubscribed: { type: 'string', description: 'Contact subscription status' },
260+
contactId: { type: 'string', description: 'Contact ID or email address' },
86261
},
87262

88263
outputs: {
89-
success: { type: 'boolean', description: 'Whether the email was sent successfully' },
264+
success: { type: 'boolean', description: 'Operation success status' },
265+
// Send email outputs
90266
to: { type: 'string', description: 'Recipient email address' },
91267
subject: { type: 'string', description: 'Email subject' },
92268
body: { type: 'string', description: 'Email body content' },
269+
// Get email outputs
270+
id: { type: 'string', description: 'Email or contact ID' },
271+
from: { type: 'string', description: 'Sender email address' },
272+
html: { type: 'string', description: 'HTML email content' },
273+
text: { type: 'string', description: 'Plain text email content' },
274+
lastEvent: { type: 'string', description: 'Last event status' },
275+
createdAt: { type: 'string', description: 'Creation timestamp' },
276+
// Contact outputs
277+
email: { type: 'string', description: 'Contact email address' },
278+
firstName: { type: 'string', description: 'Contact first name' },
279+
lastName: { type: 'string', description: 'Contact last name' },
280+
contacts: { type: 'json', description: 'Array of contacts' },
281+
// Domain outputs
282+
domains: { type: 'json', description: 'Array of domains' },
283+
hasMore: { type: 'boolean', description: 'Whether more results are available' },
284+
deleted: { type: 'boolean', description: 'Whether the resource was deleted' },
93285
},
94286
}

0 commit comments

Comments
 (0)