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 { createAction, Property } from '@activepieces/pieces-framework';
import {
imapAuth,
mailboxDropdown,
copyEmail as copyImapEmail,
} from '../common';
const props = {
sourceMailbox: mailboxDropdown({
displayName: 'Source Folder',
description: 'Folder to copy the email from.',
required: true,
}),
uid: Property.Number({
displayName: 'Message UID',
description: 'The UID of the email to copy.',
required: true,
}),
targetMailbox: mailboxDropdown({
displayName: 'Target Folder',
description: 'Folder to copy the email to.',
required: true,
}),
};
export const copyEmail = createAction({
auth: imapAuth,
name: 'copy_email',
displayName: 'Copy Email',
description: 'Copy an email to another mailbox',
props,
async run({ auth, propsValue }) {
const { uid, sourceMailbox, targetMailbox } = propsValue;
return await copyImapEmail({ auth, uid, sourceMailbox: sourceMailbox!, targetMailbox: targetMailbox! });
},
});

View File

@@ -0,0 +1,36 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { imapAuth, deleteEmail as deleteImapEmail, mailboxDropdown } from '../common';
const permanentDeletionNotice = `
**Permanent Deletion:**
This action permanently deletes the email. This action cannot be undone. To move an email to the Trash folder, use the Move Email action instead.
`;
const props = {
mailbox: mailboxDropdown({
displayName: 'Parent Folder',
description: 'Folder to delete the email from.',
required: true,
}),
uid: Property.Number({
displayName: 'Message UID',
description: 'The UID of the email to delete.',
required: true,
}),
permanentDeletionNotice: Property.MarkDown({
value: permanentDeletionNotice,
}),
};
export const deleteEmail = createAction({
auth: imapAuth,
name: 'delete_email',
displayName: 'Delete Email',
description: 'Permanently delete an email',
props,
async run({ auth, propsValue }) {
const { uid, mailbox } = propsValue;
return await deleteImapEmail({ auth, uid, mailbox: mailbox! });
},
});

View File

@@ -0,0 +1,38 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { imapAuth, setEmailReadStatus, mailboxDropdown } from '../common';
const props = {
mailbox: mailboxDropdown({
displayName: 'Parent Folder',
description: 'Select the parent folder containing the email.',
required: true,
}),
uid: Property.Number({
displayName: 'Message UID',
description: 'The UID of the email to mark.',
required: true,
}),
markAsRead: Property.Checkbox({
displayName: 'Mark as Read',
description: 'Check to mark as read, uncheck to mark as unread.',
defaultValue: true,
required: false,
}),
};
export const markEmailAsRead = createAction({
auth: imapAuth,
name: 'mark_email_read',
displayName: 'Mark Email as Read/Unread',
description: 'Sets the read status of an email',
props,
async run({ auth, propsValue }) {
const { uid, markAsRead, mailbox } = propsValue;
return await setEmailReadStatus({
auth,
uid,
mailbox: mailbox!,
markAsRead: !!markAsRead,
});
},
});

View File

@@ -0,0 +1,32 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { imapAuth, mailboxDropdown, moveEmail as moveImapEmail } from '../common';
const props = {
sourceMailbox: mailboxDropdown({
displayName: 'Source Folder',
description: 'Folder to move the email from.',
required: true,
}),
uid: Property.Number({
displayName: 'Message UID',
description: 'The UID of the email to move.',
required: true,
}),
targetMailbox: mailboxDropdown({
displayName: 'Target Folder',
description: 'Destination folder for the email.',
required: true,
}),
};
export const moveEmail = createAction({
auth: imapAuth,
name: 'move_email',
displayName: 'Move Email',
description: 'Move an email to another mailbox',
props,
async run({ auth, propsValue }) {
const { uid, sourceMailbox, targetMailbox } = propsValue;
return await moveImapEmail({ auth, uid, sourceMailbox: sourceMailbox!, targetMailbox: targetMailbox! });
},
});

View File

@@ -0,0 +1,71 @@
import {
PieceAuth,
Property,
PiecePropValueSchema,
AppConnectionValueForAuthProperty,
} from '@activepieces/pieces-framework';
import { performImapOperation } from './imap';
import { AppConnectionType } from '@activepieces/shared';
const description = `
**Gmail Users:**
<br><br>
Make Sure of the following:
<br>
* IMAP is enabled in your Gmail settings (https://support.google.com/mail/answer/7126229?hl=en)
* You have created an App Password to login with (https://support.google.com/accounts/answer/185833?hl=en)
* Enable TLS and set the port to 993 and the host to imap.gmail.com
`;
export const imapAuth = PieceAuth.CustomAuth({
description: description,
props: {
host: Property.ShortText({
displayName: 'Host',
required: true,
}),
username: Property.ShortText({
displayName: 'Username',
required: true,
}),
password: PieceAuth.SecretText({
displayName: 'Password',
required: true,
}),
port: Property.Number({
displayName: 'Port',
required: true,
defaultValue: 143,
}),
tls: Property.Checkbox({
displayName: 'Use TLS',
defaultValue: false,
required: true,
}),
validateCertificates: Property.Checkbox({
displayName: 'Validate TLS Certificates',
description:
'Enable TLS certificate validation (recommended for production).',
defaultValue: false,
required: true,
}),
},
async validate({
auth,
}): Promise<{ valid: true } | { valid: false; error: string }> {
try {
return (await performImapOperation({ type: AppConnectionType.CUSTOM_AUTH, props: auth }, async (imapClient) => {
imapClient.noop();
return { valid: true };
})) as { valid: true };
} catch (e) {
return {
valid: false,
error: e instanceof Error ? e.message : 'Unknown error',
};
}
},
required: true,
});
export type ImapAuth = AppConnectionValueForAuthProperty<typeof imapAuth>;

View File

@@ -0,0 +1,5 @@
/**
* Default lookback hours for fetching emails
* Indicates how far back in time to fetch emails on the first fetch.
*/
export const DEFAULT_LOOKBACK_HOURS = 2;

View File

@@ -0,0 +1,78 @@
export interface ImapClientError extends Error {
code?: string;
responseText?: string;
}
export class ImapError extends Error {
constructor(message: string) {
super(`IMAP error: ${message}`);
this.name = 'ImapGenericError';
}
}
export class ImapAuthenticationError extends ImapError {
constructor() {
super('Authentication failed. Check username and password.');
this.name = 'ImapAuthenticationError';
}
}
export class ImapConnectionLostError extends ImapError {
constructor() {
super('IMAP connection lost while fetching emails.');
this.name = 'ImapConnectionLostError';
}
}
export class ImapConnectionRefusedError extends ImapError {
constructor() {
super('Connection refused. Check host and port settings.');
this.name = 'ImapConnectionRefusedError';
}
}
export class ImapConnectionTimeoutError extends ImapError {
constructor() {
super(
'Connection timed out. Check network connectivity and server availability.'
);
this.name = 'ImapConnectionTimeoutError';
}
}
export class ImapCertificateError extends ImapError {
constructor() {
super(
'TLS certificate validation failed. Consider disabling certificate validation for testing.'
);
this.name = 'ImapCertificateError';
}
}
export class ImapEmailNotFoundError extends ImapError {
constructor() {
super('Email not found in the specified mailbox.');
this.name = 'ImapEmailNotFoundError';
}
}
export class ImapHostNotFoundError extends ImapError {
constructor() {
super('Host not found. Please verify the IMAP server address.');
this.name = 'ImapHostNotFoundError';
}
}
export class ImapMailboxNotFoundError extends ImapError {
constructor() {
super('The specified mailbox/folder does not exist on the server.');
this.name = 'ImapMailboxNotFoundError';
}
}
export class ImapSslPacketLengthTooLongError extends ImapError {
constructor() {
super('SSL packet length too long. The specified server port is probably incorrect.');
this.name = 'ImapSslPacketLengthTooLongError';
}
}

View File

@@ -0,0 +1,320 @@
import {
ImapFlow,
type CopyResponseObject,
type ListResponse,
type MailboxLockObject,
} from 'imapflow';
import { type Attachment, type ParsedMail, simpleParser } from 'mailparser';
import { Readable } from 'stream';
import dayjs from 'dayjs';
import { type ImapAuth } from './auth';
import { DEFAULT_LOOKBACK_HOURS } from './constants';
import {
type ImapClientError,
ImapConnectionRefusedError,
ImapHostNotFoundError,
ImapSslPacketLengthTooLongError,
ImapConnectionTimeoutError,
ImapAuthenticationError,
ImapCertificateError,
ImapError,
ImapMailboxNotFoundError,
ImapConnectionLostError,
ImapEmailNotFoundError,
} from './errors';
type Message = {
data: ParsedMail & { uid: number };
epochMilliSeconds: number;
};
function buildImapClient(auth: ImapAuth): ImapFlow {
const imapConfig = {
host: auth.props.host,
port: auth.props.port,
secure: auth.props.tls,
auth: { user: auth.props.username, pass: auth.props.password },
tls: { rejectUnauthorized: auth.props.validateCertificates },
};
return new ImapFlow({ ...imapConfig, logger: false });
}
async function confirmEmailExists(
imapClient: ImapFlow,
uid: number
): Promise<void> {
const searchResult = await imapClient.search(
{ uid: uid.toString() },
{ uid: true }
);
if (!searchResult || searchResult.length === 0) {
throw new ImapEmailNotFoundError();
}
}
function detectMissingMailbox(error: unknown): void {
if (
error &&
typeof error === 'object' &&
'mailboxMissing' in error &&
(error as { mailboxMissing: boolean }).mailboxMissing
) {
throw new ImapMailboxNotFoundError();
}
}
async function copyEmail<T extends { success: boolean; newUid?: number }>({
auth,
sourceMailbox,
targetMailbox,
uid,
}: {
auth: ImapAuth;
sourceMailbox: string;
targetMailbox: string;
uid: number;
}): Promise<T> {
return (await performMailboxOperation(
auth,
sourceMailbox,
async (imapClient) => {
await confirmEmailExists(imapClient, uid);
const result: false | CopyResponseObject = await imapClient.messageCopy(
{ uid: uid.toString() },
targetMailbox,
{ uid: true }
);
if (!result) {
throw new ImapError('Failed to copy email.');
}
const newUid = result.uidMap?.get(uid);
return { success: true, newUid };
}
)) as T;
}
async function deleteEmail<T extends { success: boolean }>({
auth,
mailbox,
uid,
}: {
auth: ImapAuth;
mailbox: string;
uid: number;
}): Promise<T> {
return (await performMailboxOperation(auth, mailbox, async (imapClient) => {
await confirmEmailExists(imapClient, uid);
await imapClient.messageDelete({ uid: uid.toString() }, { uid: true });
return { success: true };
})) as T;
}
async function fetchEmails<T extends Message[]>({
auth,
lastPoll,
mailbox,
}: {
auth: ImapAuth;
lastPoll: number;
mailbox: string;
}): Promise<T> {
return (await performMailboxOperation(auth, mailbox, async (imapClient) => {
const messages = [];
const since =
lastPoll === 0
? dayjs().subtract(DEFAULT_LOOKBACK_HOURS, 'hour').toISOString()
: dayjs(lastPoll).toISOString();
const res = imapClient.fetch({ since }, { source: true });
for await (const message of res) {
const { source, uid } = message;
const castedItem = await parseStream(source as unknown as Readable);
messages.push({
data: { ...castedItem, uid },
epochMilliSeconds: dayjs(castedItem.date).valueOf(),
});
}
return messages;
})) as T;
}
async function fetchMailboxes<T extends ListResponse[]>(
auth: ImapAuth
): Promise<T> {
return (await performImapOperation(auth, async (imapClient) => {
return await imapClient.list();
})) as T;
}
async function moveEmail<T extends { success: boolean; newUid?: number }>({
auth,
sourceMailbox,
targetMailbox,
uid,
}: {
auth: ImapAuth;
sourceMailbox: string;
targetMailbox: string;
uid: number;
}): Promise<T> {
return (await performMailboxOperation(
auth,
sourceMailbox,
async (imapClient) => {
await confirmEmailExists(imapClient, uid);
const result: false | CopyResponseObject = await imapClient.messageMove(
{ uid: uid.toString() },
targetMailbox,
{ uid: true }
);
if (result) {
const newUid = result.uidMap?.get(uid);
return { success: true, newUid };
}
return { success: false };
}
)) as T;
}
async function parseStream(stream: Readable) {
return new Promise<ParsedMail>((resolve, reject) => {
simpleParser(stream, (err, parsed) => {
if (err) {
reject(err);
} else {
resolve(parsed);
}
});
});
}
async function performImapOperation(
auth: ImapAuth,
callback: (imapClient: ImapFlow) => Promise<unknown>
) {
let imapClient: ImapFlow | null = null;
try {
imapClient = buildImapClient(auth);
await imapClient.connect();
return await callback(imapClient);
} catch (error) {
const imapError = error as ImapClientError;
if (imapError.code === 'ECONNREFUSED') {
throw new ImapConnectionRefusedError();
} else if (imapError.code === 'ENOTFOUND') {
throw new ImapHostNotFoundError();
} else if (imapError.code === 'ETIMEDOUT') {
throw new ImapConnectionTimeoutError();
} else if (imapError.code === 'ERR_SSL_PACKET_LENGTH_TOO_LONG') {
throw new ImapSslPacketLengthTooLongError();
} else if (imapError.responseText?.includes('AUTH')) {
throw new ImapAuthenticationError();
} else if (imapError.message?.includes('IMAP connection')) {
throw new ImapConnectionLostError();
} else if (imapError.message?.includes('certificate')) {
throw new ImapCertificateError();
} else if (imapError instanceof ImapError) {
throw imapError;
}
throw new ImapError(
imapError.message || 'Failed to perform IMAP operation'
);
} finally {
try {
if (imapClient?.usable) {
await imapClient.logout();
}
} catch (e) {
// Ignore logout errors during cleanup
}
}
}
async function performMailboxOperation<T>(
auth: ImapAuth,
mailbox: string,
callback: (imapClient: ImapFlow) => Promise<T>,
options: { readOnly?: boolean } = {}
) {
const { readOnly = true } = options;
return (await performImapOperation(auth, async (imapClient) => {
let lock: MailboxLockObject | null = null;
try {
lock = await imapClient.getMailboxLock(mailbox, { readOnly: readOnly });
return await callback(imapClient);
} catch (error) {
detectMissingMailbox(error);
throw error;
} finally {
try {
lock?.release();
} catch (e) {
// Ignore lock release errors during cleanup
}
}
})) as T;
}
async function setEmailReadStatus<T extends { success: true }>({
auth,
mailbox,
uid,
markAsRead,
}: {
auth: ImapAuth;
mailbox: string;
uid: number;
markAsRead: boolean;
}): Promise<T> {
return (await performMailboxOperation(
auth,
mailbox,
async (imapClient) => {
await confirmEmailExists(imapClient, uid);
if (markAsRead) {
await imapClient.messageFlagsAdd({ uid: uid.toString() }, ['\\Seen'], { uid: true });
} else {
await imapClient.messageFlagsRemove({ uid: uid.toString() }, ['\\Seen'], { uid: true });
}
return { success: true };
},
{ readOnly: false }
)) as T;
}
export {
// Types
type Attachment,
type Message,
// Helper functions
performImapOperation,
performMailboxOperation,
// Email actions
copyEmail,
deleteEmail,
fetchEmails,
moveEmail,
setEmailReadStatus,
// Mailbox actions
fetchMailboxes,
};

View File

@@ -0,0 +1,5 @@
export * from './auth';
export * from './constants';
export * from './errors';
export * from './imap';
export * from './props';

View File

@@ -0,0 +1,45 @@
import { Property } from '@activepieces/pieces-framework';
import { fetchMailboxes } from './imap';
import { imapAuth, type ImapAuth } from './auth';
interface DropdownParams {
description?: string;
displayName: string;
required: boolean;
}
export const mailboxDropdown = (params: DropdownParams) =>
Property.Dropdown<string,boolean,typeof imapAuth>({
auth: imapAuth,
displayName: params.displayName,
description: params.description,
required: params.required,
refreshers: [],
async options({ auth }) {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your account first',
};
}
try {
const mailboxes = await fetchMailboxes(auth);
const options = mailboxes.map(
({ name, path }: { name: string; path: string }) => ({
label: name,
value: path,
})
);
return { disabled: false, options };
} catch (error: any) {
return {
disabled: true,
options: [],
placeholder: `Error: ${error.message}`,
};
}
},
});

View File

@@ -0,0 +1,165 @@
import {
DedupeStrategy,
Polling,
pollingHelper,
} from '@activepieces/pieces-common';
import {
AppConnectionValueForAuthProperty,
FilesService,
PiecePropValueSchema,
Property,
StaticPropsValue,
TriggerStrategy,
createTrigger,
} from '@activepieces/pieces-framework';
import {
type Attachment,
type Message,
imapAuth,
mailboxDropdown,
fetchEmails,
} from '../common';
const filterInstructions = `
**Emails Filtering:**
Add a Router Piece to filter emails based on the subject, to, from, cc or other fields.
`;
const props = {
mailbox: mailboxDropdown({
displayName: 'Mailbox',
description: 'Select the mailbox to search.',
required: true,
}),
filterInstructions: Property.MarkDown({
value: filterInstructions,
}),
};
const polling: Polling<
AppConnectionValueForAuthProperty<typeof imapAuth>,
StaticPropsValue<typeof props>
> = {
strategy: DedupeStrategy.TIMEBASED,
items: async ({ auth, propsValue, lastFetchEpochMS: lastPoll }) => {
const { mailbox } = propsValue;
const records = await fetchEmails({
auth,
lastPoll,
mailbox: mailbox as string,
});
return records.map((record) => ({
epochMilliSeconds: record.epochMilliSeconds,
data: record,
}));
},
};
// This wrapper's only purpose is to reverse the messages array to ensure that
// test polling returns the 5 most recent messages.
const testPolling: typeof polling = {
...polling,
items: async (...args) => {
const messages = await polling.items(...args);
return messages.reverse();
},
};
export const newEmail = createTrigger({
auth: imapAuth,
name: 'new_email',
displayName: 'New Email',
description: 'Trigger when a new email is received',
props,
type: TriggerStrategy.POLLING,
async test(context) {
const messages = await pollingHelper.test(testPolling, context);
return enrichAttachments(messages as Message[], context.files);
},
async onEnable(context) {
const { store, auth, propsValue } = context;
await pollingHelper.onEnable(polling, { store, auth, propsValue });
},
async onDisable(context) {
const { store, auth, propsValue } = context;
await pollingHelper.onDisable(polling, { store, auth, propsValue });
},
async run(context) {
const messages = await pollingHelper.poll(polling, context);
return enrichAttachments(messages as Message[], context.files);
},
sampleData: {
html: '<p>My email body</p>',
text: 'My email body',
attachments: [],
textAsHtml: '<p>My email body</p>',
subject: 'Email Subject',
date: '2023-06-18T11:30:09.000Z',
to: {
value: [
{
address: 'email@address.com',
name: 'Name',
},
],
},
from: {
value: [
{
address: 'email@address.com',
name: 'Name',
},
],
},
cc: {
value: [
{
address: 'email@address.com',
name: 'Name',
},
],
},
messageId:
'<CxE49ifJT5YZN9OE2O6j6Ef+BYgkKWq7X-deg483GkM1ui1xj3g@mail.gmail.com>',
uid: 123,
},
});
export async function convertAttachment(
attachments: Attachment[],
files: FilesService
) {
const promises = attachments.map(async (attachment) => {
return files.write({
fileName: attachment.filename ?? `attachment-${Date.now()}`,
data: attachment.content,
});
});
return Promise.all(promises);
}
async function enrichAttachments(items: Message[], files: FilesService) {
return Promise.all(
items.map(async (item) => {
const { attachments, ...rest } = item.data;
const convertedAttachments = attachments
? await convertAttachment(attachments, files)
: [];
return {
...rest,
attachments: convertedAttachments,
// epochMilliSeconds: item.epochMilliSeconds,
};
})
);
}