Add Activepieces integration for workflow automation

- Add Activepieces fork with SmoothSchedule custom piece
- Create integrations app with Activepieces service layer
- Add embed token endpoint for iframe integration
- Create Automations page with embedded workflow builder
- Add sidebar visibility fix for embed mode
- Add list inactive customers endpoint to Public API
- Include SmoothSchedule triggers: event created/updated/cancelled
- Include SmoothSchedule actions: create/update/cancel events, list resources/services/customers

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-18 22:59:37 -05:00
parent 9848268d34
commit 3aa7199503
16292 changed files with 1284892 additions and 4708 deletions

View File

@@ -0,0 +1,36 @@
import { HttpMethod } from '@activepieces/pieces-common';
import { createAction } from '@activepieces/pieces-framework';
import { zohoMailApiCall } from '../common';
import { zohoMailAuth } from '../common/auth';
import { accountId, folderId, messageId } from '../common/props';
export const archiveEmailAction = createAction({
auth: zohoMailAuth,
name: 'archive_email',
displayName: 'Archive Email',
description: 'Archives an email.',
props: {
accountId: accountId({ displayName: 'Account', required: true }),
folderId: folderId({ displayName: 'Folder', required: true }),
messageId: messageId({
displayName: 'Message ID',
description: 'The ID of the email message to archive.',
required: true,
}),
},
async run(context) {
const { accountId, messageId } = context.propsValue;
const response = await zohoMailApiCall({
auth: context.auth,
method: HttpMethod.PUT,
resourceUri: `/accounts/${accountId}/updatemessage`,
body: {
mode: 'archiveMails',
messageId: [messageId],
},
});
return response;
},
});

View File

@@ -0,0 +1,38 @@
import { HttpMethod } from '@activepieces/pieces-common';
import { createAction } from '@activepieces/pieces-framework';
import { convertAttachment, parseStream, zohoMailApiCall } from '../common';
import { zohoMailAuth } from '../common/auth';
import { accountId, folderId, messageId } from '../common/props';
export const getEmailDetailsAction = createAction({
auth: zohoMailAuth,
name: 'get_email_details',
displayName: 'Get Email Details',
description: 'Retrieves full content and metadata of a specific email.',
props: {
accountId: accountId({ displayName: 'Account', required: true }),
folderId: folderId({ displayName: 'Folder', required: true }),
messageId: messageId({
displayName: 'Message ID',
description: 'The ID of the email message to retrieve.',
required: true,
}),
},
async run(context) {
const { accountId, messageId } = context.propsValue;
const response = await zohoMailApiCall<{ data: { content: string; messageId: string } }>({
auth: context.auth,
method: HttpMethod.GET,
resourceUri: `/accounts/${accountId}/messages/${messageId}/originalmessage`,
});
const parsedMailResponse = await parseStream(response.data.content);
return {
...parsedMailResponse,
attachments: await convertAttachment(parsedMailResponse.attachments, context.files),
id: response.data.messageId,
};
},
});

View File

@@ -0,0 +1,36 @@
import { HttpMethod } from '@activepieces/pieces-common';
import { createAction } from '@activepieces/pieces-framework';
import { zohoMailApiCall } from '../common';
import { zohoMailAuth } from '../common/auth';
import { accountId, folderId, messageId } from '../common/props';
export const markEmailAsReadAction = createAction({
auth: zohoMailAuth,
name: 'mark_email_as_read',
displayName: 'Mark Email as Read',
description: 'Marks an email as read.',
props: {
accountId: accountId({ displayName: 'Account', required: true }),
folderId: folderId({ displayName: 'Folder', required: true }),
messageId: messageId({
displayName: 'Message ID',
description: 'The ID of the email message to mark as read.',
required: true,
}),
},
async run(context) {
const { accountId, messageId } = context.propsValue;
const response = await zohoMailApiCall({
auth: context.auth,
method: HttpMethod.PUT,
resourceUri: `/accounts/${accountId}/updatemessage`,
body: {
mode: 'markAsRead',
messageId: [messageId],
},
});
return response;
},
});

View File

@@ -0,0 +1,36 @@
import { HttpMethod } from '@activepieces/pieces-common';
import { createAction } from '@activepieces/pieces-framework';
import { zohoMailApiCall } from '../common';
import { zohoMailAuth } from '../common/auth';
import { accountId, folderId, messageId } from '../common/props';
export const markEmailAsUnreadAction = createAction({
auth: zohoMailAuth,
name: 'mark_email_as_unread',
displayName: 'Mark Emai as Unread',
description: 'Marks an email as unread.',
props: {
accountId: accountId({ displayName: 'Account', required: true }),
folderId: folderId({ displayName: 'Folder', required: true }),
messageId: messageId({
displayName: 'Message ID',
description: 'The ID of the email message to mark as unread.',
required: true,
}),
},
async run(context) {
const { accountId, messageId } = context.propsValue;
const response = await zohoMailApiCall({
auth: context.auth,
method: HttpMethod.PUT,
resourceUri: `/accounts/${accountId}/updatemessage`,
body: {
mode: 'markAsUnread',
messageId: [messageId],
},
});
return response;
},
});

View File

@@ -0,0 +1,45 @@
import { HttpMethod } from '@activepieces/pieces-common';
import { createAction } from '@activepieces/pieces-framework';
import { zohoMailApiCall } from '../common';
import { zohoMailAuth } from '../common/auth';
import { accountId, folderId, messageId } from '../common/props';
export const moveEmailAction = createAction({
auth: zohoMailAuth,
name: 'move_email',
displayName: 'Move Email to Folder',
description: 'Moves an email to a different folder.',
props: {
accountId: accountId({ displayName: 'Account', required: true }),
folderId: folderId({ displayName: 'Current Folder', required: true }),
messageId: messageId({
displayName: 'Message ID',
description: 'The ID of the email message to move.',
required: true,
}),
destfolderId: folderId({
displayName: 'Destination Folder',
description: 'Select the folder to move the email to.',
required: true,
}),
},
async run(context) {
const { accountId, destfolderId, messageId, folderId } = context.propsValue;
const response = await zohoMailApiCall({
auth: context.auth,
method: HttpMethod.PUT,
resourceUri: `/accounts/${accountId}/updatemessage`,
body: {
mode: 'moveMessage',
destfolderId: destfolderId,
messageId: [messageId],
isFolderSpecific: true,
folderId: folderId,
},
});
return response;
},
});

View File

@@ -0,0 +1,136 @@
import { HttpMethod, httpClient } from '@activepieces/pieces-common';
import { Property, createAction } from '@activepieces/pieces-framework';
import FormData from 'form-data';
import { zohoMailApiCall } from '../common';
import { zohoMailAuth } from '../common/auth';
import { accountId, fromAddress } from '../common/props';
export const sendEmailAction = createAction({
auth: zohoMailAuth,
name: 'send_email',
displayName: 'Send Email',
description: 'Sends an email.',
props: {
accountId: accountId({ displayName: 'Account', required: true }),
fromAddress: fromAddress({ displayName: 'From Email Address', required: true }),
toAddress: Property.ShortText({
displayName: 'To Email Address',
description: "Recipient's email address.",
required: true,
}),
subject: Property.LongText({
displayName: 'Subject',
required: true,
}),
mailFormat: Property.StaticDropdown({
displayName: 'Mail Format',
required: true,
options: {
options: [
{ label: 'HTML', value: 'html' },
{ label: 'Plain Text', value: 'plaintext' },
],
},
defaultValue: 'html',
}),
content: Property.LongText({
displayName: 'Content',
description: 'HTML or plain text content of the email.',
required: true,
}),
ccAddress: Property.ShortText({
displayName: 'CC Email Address',
description: "CC recipient's email address.",
required: false,
}),
bccAddress: Property.ShortText({
displayName: 'BCC Email Address',
description: "BCC recipient's email address.",
required: false,
}),
askReceipt: Property.StaticDropdown({
displayName: 'Ask for Read Receipt',
required: false,
options: {
options: [
{ label: 'Yes', value: 'yes' },
{ label: 'No', value: 'no' },
],
},
}),
attachment: Property.File({
displayName: 'Attachment',
required: false,
}),
attachmentName: Property.ShortText({
displayName: 'Attachment Name',
description: 'In case you want to change the name of the attachment.',
required: false,
}),
},
async run(context) {
const {
accountId,
fromAddress,
toAddress,
ccAddress,
bccAddress,
subject,
content,
mailFormat,
askReceipt,
attachment,
attachmentName,
} = context.propsValue;
const requestBody: Record<string, unknown> = {
fromAddress,
toAddress,
subject,
content,
mailFormat: mailFormat ?? 'html',
};
if (ccAddress) requestBody['ccAddress'] = ccAddress;
if (bccAddress) requestBody['bccAddress'] = bccAddress;
if (askReceipt) requestBody['askReceipt'] = askReceipt;
if (attachment) {
const formData = new FormData();
formData.append(
'attach',
Buffer.from(attachment.base64, 'base64'),
attachmentName || attachment.filename,
);
const location = context.auth.props?.['location'] ?? 'zoho.com';
const baseUrl = `https://mail.${location}/api`;
const uploadResponse = await httpClient.sendRequest<{
data: { storeName: string; attachmentName: string; attachmentPath: string }[];
}>({
url: baseUrl + `/accounts/${accountId}/messages/attachments?uploadType=multipart`,
method: HttpMethod.POST,
body: formData,
headers: {
...formData.getHeaders(),
Authorization: `Zoho-oauthtoken ${context.auth.access_token}`,
},
});
requestBody['attachments'] = uploadResponse.body.data;
}
const response = await zohoMailApiCall({
auth: context.auth,
method: HttpMethod.POST,
resourceUri: `/accounts/${accountId}/messages`,
body: requestBody,
});
return response;
},
});

View File

@@ -0,0 +1,36 @@
import { HttpMethod } from '@activepieces/pieces-common';
import { createAction } from '@activepieces/pieces-framework';
import { zohoMailApiCall } from '../common';
import { zohoMailAuth } from '../common/auth';
import { accountId, folderId, messageId } from '../common/props';
export const unarchiveEmailAction = createAction({
auth: zohoMailAuth,
name: 'unarchive_email',
displayName: 'Unarchive Email',
description: 'Unarchives an email.',
props: {
accountId: accountId({ displayName: 'Account', required: true }),
folderId: folderId({ displayName: 'Folder', required: true }),
messageId: messageId({
displayName: 'Message ID',
description: 'The ID of the email message to unarchive.',
required: true,
}),
},
async run(context) {
const { accountId, messageId } = context.propsValue;
const response = await zohoMailApiCall({
auth: context.auth,
method: HttpMethod.PUT,
resourceUri: `/accounts/${accountId}/updatemessage`,
body: {
mode: 'unArchiveMails',
messageId: [messageId],
},
});
return response;
},
});

View File

@@ -0,0 +1,49 @@
import { PieceAuth, Property } from '@activepieces/pieces-framework';
export const zohoMailAuth = PieceAuth.OAuth2({
props: {
location: Property.StaticDropdown({
displayName: 'Location',
description: 'The location of your Zoho Mail account.',
required: true,
options: {
options: [
{
label: 'zoho.eu (Europe)',
value: 'zoho.eu',
},
{
label: 'zoho.com (United States)',
value: 'zoho.com',
},
{
label: 'zoho.com.au (Australia)',
value: 'zoho.com.au',
},
{
label: 'zoho.jp (Japan)',
value: 'zoho.jp',
},
{
label: 'zoho.in (India)',
value: 'zoho.in',
},
{
label: 'zohocloud.ca (Canada)',
value: 'zohocloud.ca',
},
],
},
}),
},
description: 'Authentication for Zoho Desk',
scope: [
'ZohoMail.accounts.READ',
'ZohoMail.messages.ALL',
'ZohoMail.folders.ALL',
'ZohoMail.organization.accounts.READ',
],
authUrl: 'https://accounts.{location}/oauth/v2/auth',
tokenUrl: 'https://accounts.{location}/oauth/v2/token',
required: true,
});

View File

@@ -0,0 +1,85 @@
import {
HttpMessageBody,
HttpMethod,
HttpRequest,
QueryParams,
httpClient,
} from '@activepieces/pieces-common';
import { FilesService, PiecePropValueSchema } from '@activepieces/pieces-framework';
import { Attachment, ParsedMail, simpleParser } from 'mailparser';
import { zohoMailAuth } from './auth';
export type ZohoMailApiCallParams = {
auth: PiecePropValueSchema<typeof zohoMailAuth>;
method: HttpMethod;
resourceUri: string;
query?: Record<string, string | number | string[] | undefined>;
body?: any;
};
export async function zohoMailApiCall<T extends HttpMessageBody>({
auth,
method,
resourceUri,
query,
body,
}: ZohoMailApiCallParams): Promise<T> {
const location = auth.props?.['location'] ?? 'zoho.com';
const baseUrl = `https://mail.${location}/api`;
const qs: QueryParams = {};
if (query) {
for (const [key, value] of Object.entries(query)) {
if (value !== null && value !== undefined) {
qs[key] = String(value);
}
}
}
const request: HttpRequest = {
method,
url: baseUrl + resourceUri,
headers: {
Authorization: `Zoho-oauthtoken ${auth.access_token}`,
},
queryParams: qs,
body,
};
const response = await httpClient.sendRequest<T>(request);
return response.body;
}
export async function parseStream(stream: string | Buffer): Promise<ParsedMail> {
return new Promise<ParsedMail>((resolve, reject) => {
simpleParser(stream, (err, parsed) => {
if (err) {
reject(err);
} else {
resolve(parsed);
}
});
});
}
export async function convertAttachment(attachments: Attachment[], files: FilesService) {
const promises = attachments.map(async (attachment) => {
try {
const fileName = attachment.filename ?? `attachment-${Date.now()}`;
return {
fileName,
mimeType: attachment.contentType,
size: attachment.size,
data: await files.write({
fileName: fileName,
data: attachment.content,
}),
};
} catch (error) {
console.error(`Failed to process attachment: ${attachment.filename}`, error);
return null;
}
});
const results = await Promise.all(promises);
return results.filter((result) => result !== null);
}

View File

@@ -0,0 +1,192 @@
import { HttpMethod } from '@activepieces/pieces-common';
import { PiecePropValueSchema, Property } from '@activepieces/pieces-framework';
import { zohoMailApiCall } from '.';
import { zohoMailAuth } from './auth';
interface DropdownParams {
displayName: string;
description?: string;
required: boolean;
}
export const accountId = (params: DropdownParams) =>
Property.Dropdown({
auth: zohoMailAuth,
displayName: params.displayName,
description: params.description,
refreshers: [],
required: params.required,
options: async ({ auth }) => {
if (!auth) {
return {
placeholder: 'Please connect your account first.',
options: [],
disabled: true,
};
}
const authValue = auth as PiecePropValueSchema<typeof zohoMailAuth>;
const response = await zohoMailApiCall<{
data: { accountId: string; displayName: string }[];
}>({
auth: authValue,
method: HttpMethod.GET,
resourceUri: '/accounts',
});
return {
disabled: false,
options: response.data.map((account) => {
return {
label: account.displayName || account.accountId,
value: account.accountId,
};
}),
};
},
});
export const folderId = (params: DropdownParams) =>
Property.Dropdown({
auth: zohoMailAuth,
displayName: params.displayName,
description: params.description,
refreshers: ['accountId'],
required: params.required,
options: async ({ auth, accountId }) => {
if (!auth) {
return {
placeholder: 'Please connect your account first.',
options: [],
disabled: true,
};
}
if (!accountId) {
return {
placeholder: 'Please select Account first.',
options: [],
disabled: true,
};
}
const authValue = auth as PiecePropValueSchema<typeof zohoMailAuth>;
const response = await zohoMailApiCall<{
data: { folderId: string; path: string }[];
}>({
auth: authValue,
method: HttpMethod.GET,
resourceUri: `/accounts/${accountId}/folders`,
});
return {
disabled: false,
options: response.data.map((folder) => {
return {
label: folder.path || folder.folderId,
value: folder.folderId,
};
}),
};
},
});
export const messageId = (params: DropdownParams) =>
Property.Dropdown({
auth: zohoMailAuth,
displayName: params.displayName,
description: params.description,
refreshers: ['accountId', 'folderId'],
required: params.required,
options: async ({ auth, accountId, folderId }) => {
if (!auth) {
return {
placeholder: 'Please connect your account first.',
options: [],
disabled: true,
};
}
if (!accountId) {
return {
placeholder: 'Please select Account first.',
options: [],
disabled: true,
};
}
if (!folderId) {
return {
placeholder: 'Please select Folder first.',
options: [],
disabled: true,
};
}
const authValue = auth as PiecePropValueSchema<typeof zohoMailAuth>;
const response = await zohoMailApiCall<{
data: { messageId: string; subject: string }[];
}>({
auth: authValue,
method: HttpMethod.GET,
resourceUri: `/accounts/${accountId}/messages/view`,
query: {
folderId: folderId as string,
limit: 50,
},
});
return {
disabled: false,
options: response.data.map((message) => {
return {
label: message.subject,
value: message.messageId,
};
}),
};
},
});
export const fromAddress = (params: DropdownParams) =>
Property.Dropdown({
auth: zohoMailAuth,
displayName: params.displayName,
description: params.description,
refreshers: ['accountId'],
required: params.required,
options: async ({ auth, accountId }) => {
if (!auth) {
return {
placeholder: 'Please connect your account first.',
options: [],
disabled: true,
};
}
if (!accountId) {
return {
placeholder: 'Please select Account first.',
options: [],
disabled: true,
};
}
const authValue = auth as PiecePropValueSchema<typeof zohoMailAuth>;
const response = await zohoMailApiCall<{
data: { sendMailDetails: { fromAddress: string }[] };
}>({
auth: authValue,
method: HttpMethod.GET,
resourceUri: `/accounts/${accountId}`,
});
return {
disabled: false,
options: response.data.sendMailDetails.map((account) => {
return {
label: account.fromAddress,
value: account.fromAddress,
};
}),
};
},
});

View File

@@ -0,0 +1,138 @@
import {
DedupeStrategy,
HttpMethod,
Polling,
QueryParams,
pollingHelper,
} from '@activepieces/pieces-common';
import {
AppConnectionValueForAuthProperty,
PiecePropValueSchema,
TriggerStrategy,
createTrigger,
} from '@activepieces/pieces-framework';
import { zohoMailApiCall } from '../common';
import { zohoMailAuth } from '../common/auth';
import { accountId, folderId } from '../common/props';
type Props = {
accountId?: string;
folderId?: string;
};
const polling: Polling<AppConnectionValueForAuthProperty<typeof zohoMailAuth>, Props> = {
strategy: DedupeStrategy.TIMEBASED,
async items({ auth, propsValue, lastFetchEpochMS }) {
const { accountId, folderId } = propsValue;
let page = 1;
let hasMore = true;
const allMessages = [];
do {
const queryParams: QueryParams = {
start: page.toString(),
limit: lastFetchEpochMS === 0 ? '10' : '200',
};
if (folderId) {
queryParams['folderId'] = folderId;
}
const response = await zohoMailApiCall<{ data: { receivedTime: string }[] }>({
auth,
resourceUri: `/accounts/${accountId}/messages/view`,
method: HttpMethod.GET,
query: queryParams,
});
const messages = response.data || [];
if (messages.length === 0) {
break;
}
for (const msg of messages) {
const receivedTime = Number(msg.receivedTime);
if (lastFetchEpochMS > 0 && receivedTime <= lastFetchEpochMS) {
hasMore = false;
break; // Stop processing this page
}
allMessages.push(msg); // Only add if it's newer
}
// if it's test mode, only fetch first page
if (lastFetchEpochMS === 0) break;
if (!hasMore) {
break;
}
page++;
} while (hasMore);
return allMessages.map((msg) => {
return {
epochMilliSeconds: Number(msg.receivedTime),
data: msg,
};
});
},
};
export const newEmailReceivedTrigger = createTrigger({
auth: zohoMailAuth,
name: 'new_email_received',
displayName: 'New Email Received',
description: 'Triggers when a new email is received in a specified folder (or inbox).',
props: {
accountId: accountId({ displayName: 'Account', required: true }),
folderId: folderId({
displayName: 'Folder',
description:
'Select the folder to watch. If empty, watches the inbox/all messages based on API default.',
required: false,
}),
},
type: TriggerStrategy.POLLING,
async onEnable(context) {
await pollingHelper.onEnable(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
});
},
async onDisable(context) {
await pollingHelper.onDisable(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
});
},
async test(context) {
return await pollingHelper.test(polling, context);
},
async run(context) {
return await pollingHelper.poll(polling, context);
},
sampleData: {
summary: 'test mail',
sentDateInGMT: '1749273996000',
calendarType: 0,
subject: 'test mail',
messageId: '1749293811021114900',
flagid: 'flag_not_set',
status2: '0',
priority: '3',
hasInline: 'false',
toAddress: '',
folderId: '7723149000000002014',
ccAddress: 'Not Provided',
hasAttachment: '0',
size: '238',
sender: 'john.doe@gmail.com',
receivedTime: '1749293811018',
fromAddress: 'john.doe@gmail.com',
status: '0',
},
});