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,33 @@
{
"extends": [
"../../../../.eslintrc.base.json"
],
"ignorePatterns": [
"!**/*"
],
"overrides": [
{
"files": [
"*.ts",
"*.tsx",
"*.js",
"*.jsx"
],
"rules": {}
},
{
"files": [
"*.ts",
"*.tsx"
],
"rules": {}
},
{
"files": [
"*.js",
"*.jsx"
],
"rules": {}
}
]
}

View File

@@ -0,0 +1,7 @@
# pieces-amazon-ses
This library was generated with [Nx](https://nx.dev).
## Building
Run `nx build pieces-amazon-ses` to build the library.

View File

@@ -0,0 +1,7 @@
{
"name": "@activepieces/piece-amazon-ses",
"version": "0.0.3",
"dependencies": {
"@aws-sdk/client-ses": "3.864.0"
}
}

View File

@@ -0,0 +1,51 @@
{
"name": "pieces-amazon-ses",
"$schema": "../../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/pieces/community/amazon-ses/src",
"projectType": "library",
"targets": {
"build": {
"executor": "@nx/js:tsc",
"outputs": [
"{options.outputPath}"
],
"options": {
"outputPath": "dist/packages/pieces/community/amazon-ses",
"tsConfig": "packages/pieces/community/amazon-ses/tsconfig.lib.json",
"packageJson": "packages/pieces/community/amazon-ses/package.json",
"main": "packages/pieces/community/amazon-ses/src/index.ts",
"assets": [
"packages/pieces/community/amazon-ses/*.md",
{
"input": "packages/pieces/community/amazon-ses/src/i18n",
"output": "./src/i18n",
"glob": "**/!(i18n.json)"
}
],
"buildableProjectDepsInPackageJsonType": "dependencies",
"updateBuildableProjectDepsInPackageJson": true
},
"dependsOn": [
"prebuild",
"^build"
]
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": [
"{options.outputFile}"
]
},
"prebuild": {
"executor": "nx:run-commands",
"options": {
"cwd": "packages/pieces/community/amazon-ses",
"command": "bun install --no-save --silent"
},
"dependsOn": [
"^build"
]
}
},
"tags": []
}

View File

@@ -0,0 +1,193 @@
import {
createPiece,
PieceAuth,
Property,
} from '@activepieces/pieces-framework';
import { SES, GetSendQuotaCommand } from '@aws-sdk/client-ses';
import { sendEmail } from './lib/actions/send-email';
import { createEmailTemplate } from './lib/actions/create-email-template';
import { sendTemplatedEmail } from './lib/actions/send-templated-email';
import { updateEmailTemplate } from './lib/actions/update-email-template';
import { createCustomVerificationEmailTemplate } from './lib/actions/create-custom-verification-email-template';
import { sendCustomVerificationEmail } from './lib/actions/send-custom-verification-email';
import { updateCustomVerificationEmailTemplate } from './lib/actions/update-custom-verification-email-template';
export const amazonSesAuth = PieceAuth.CustomAuth({
props: {
accessKeyId: Property.ShortText({
displayName: 'Access Key ID',
required: true,
}),
secretAccessKey: PieceAuth.SecretText({
displayName: 'Secret Access Key',
required: true,
}),
region: Property.StaticDropdown({
displayName: 'Region',
options: {
options: [
{
label: 'Default',
value: 'us-east-1',
},
{
label: 'US East (N. Virginia) [us-east-1]',
value: 'us-east-1',
},
{
label: 'US East (Ohio) [us-east-2]',
value: 'us-east-2',
},
{
label: 'US West (N. California) [us-west-1]',
value: 'us-west-1',
},
{
label: 'US West (Oregon) [us-west-2]',
value: 'us-west-2',
},
{
label: 'Africa (Cape Town) [af-south-1]',
value: 'af-south-1',
},
{
label: 'Asia Pacific (Hong Kong) [ap-east-1]',
value: 'ap-east-1',
},
{
label: 'Asia Pacific (Mumbai) [ap-south-1]',
value: 'ap-south-1',
},
{
label: 'Asia Pacific (Osaka-Local) [ap-northeast-3]',
value: 'ap-northeast-3',
},
{
label: 'Asia Pacific (Seoul) [ap-northeast-2]',
value: 'ap-northeast-2',
},
{
label: 'Asia Pacific (Singapore) [ap-southeast-1]',
value: 'ap-southeast-1',
},
{
label: 'Asia Pacific (Sydney) [ap-southeast-2]',
value: 'ap-southeast-2',
},
{
label: 'Asia Pacific (Tokyo) [ap-northeast-1]',
value: 'ap-northeast-1',
},
{
label: 'Canada (Central) [ca-central-1]',
value: 'ca-central-1',
},
{
label: 'Europe (Frankfurt) [eu-central-1]',
value: 'eu-central-1',
},
{
label: 'Europe (Ireland) [eu-west-1]',
value: 'eu-west-1',
},
{
label: 'Europe (London) [eu-west-2]',
value: 'eu-west-2',
},
{
label: 'Europe (Milan) [eu-south-1]',
value: 'eu-south-1',
},
{
label: 'Europe (Paris) [eu-west-3]',
value: 'eu-west-3',
},
{
label: 'Europe (Stockholm) [eu-north-1]',
value: 'eu-north-1',
},
{
label: 'Middle East (Bahrain) [me-south-1]',
value: 'me-south-1',
},
{
label: 'South America (São Paulo) [sa-east-1]',
value: 'sa-east-1',
},
{
label: 'Europe (Spain) [eu-south-2]',
value: 'eu-south-2',
},
{
label: 'Asia Pacific (Hyderabad) [ap-south-2]',
value: 'ap-south-2',
},
{
label: 'Asia Pacific (Jakarta) [ap-southeast-3]',
value: 'ap-southeast-3',
},
{
label: 'Asia Pacific (Melbourne) [ap-southeast-4]',
value: 'ap-southeast-4',
},
{
label: 'China (Beijing) [cn-north-1]',
value: 'cn-north-1',
},
{
label: 'China (Ningxia) [cn-northwest-1]',
value: 'cn-northwest-1',
},
{
label: 'Europe (Zurich) [eu-central-2]',
value: 'eu-central-2',
},
{
label: 'Middle East (UAE) [me-central-1]',
value: 'me-central-1',
},
],
},
required: true,
}),
},
validate: async ({ auth }) => {
try {
const ses = new SES({
credentials: {
accessKeyId: auth.accessKeyId,
secretAccessKey: auth.secretAccessKey,
},
region: auth.region,
});
await ses.send(new GetSendQuotaCommand({}));
return {
valid: true,
};
} catch (e) {
return {
valid: false,
error: (e as Error)?.message,
};
}
},
required: true,
});
export const amazonSes = createPiece({
displayName: 'Amazon SES',
auth: amazonSesAuth,
minimumSupportedRelease: '0.36.1',
logoUrl: 'https://cdn.activepieces.com/pieces/amazon-ses.png',
authors: ["fortunamide"],
actions: [
sendEmail,
createEmailTemplate,
sendTemplatedEmail,
updateEmailTemplate,
createCustomVerificationEmailTemplate,
sendCustomVerificationEmail,
updateCustomVerificationEmailTemplate,
],
triggers: [],
});

View File

@@ -0,0 +1,195 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import {
SESClient,
CreateCustomVerificationEmailTemplateCommand,
} from '@aws-sdk/client-ses';
import { amazonSesAuth } from '../../index';
import {
getVerifiedIdentities,
getCustomVerificationTemplates,
createSESClient,
validateCustomVerificationTemplateName,
validateURL,
validateCustomVerificationContent,
getCustomVerificationErrorMessage,
formatContentSize,
createIdentityDropdownOptions,
isValidEmail,
} from '../common/ses-utils';
export const createCustomVerificationEmailTemplate = createAction({
auth: amazonSesAuth,
name: 'create_custom_verification_email_template',
displayName: 'Create Custom Verification Email Template',
description: 'Create custom email template for identity verification',
props: {
templateName: Property.ShortText({
displayName: 'Template Name',
description:
'Unique template name (letters, numbers, underscores, hyphens only)',
required: true,
}),
fromEmailAddress: Property.Dropdown({
auth: amazonSesAuth,
displayName: 'From Email',
description: 'Verified sender email address',
required: true,
refreshers: [],
options: async ({ auth }) => {
const verifiedIdentities = await getVerifiedIdentities(auth as any);
return createIdentityDropdownOptions(verifiedIdentities);
},
}),
templateSubject: Property.ShortText({
displayName: 'Subject',
description: 'Email subject for verification messages',
required: true,
}),
templateContent: Property.LongText({
displayName: 'Email Content',
description:
'HTML content for verification email (must include verification link)',
required: true,
}),
successRedirectionURL: Property.ShortText({
displayName: 'Success Redirect URL',
description: 'URL to redirect users after successful verification',
required: true,
}),
failureRedirectionURL: Property.ShortText({
displayName: 'Failure Redirect URL',
description: 'URL to redirect users if verification fails',
required: true,
}),
checkExisting: Property.Checkbox({
displayName: 'Check if Template Exists',
description: 'Verify template name is unique before creating',
required: false,
defaultValue: true,
}),
validateUrls: Property.Checkbox({
displayName: 'Validate URLs',
description: 'Check that redirect URLs are properly formatted',
required: false,
defaultValue: true,
}),
},
async run(context) {
const {
templateName,
fromEmailAddress,
templateSubject,
templateContent,
successRedirectionURL,
failureRedirectionURL,
checkExisting,
validateUrls,
} = context.propsValue;
const { accessKeyId, secretAccessKey, region } = context.auth.props;
validateCustomVerificationTemplateName(templateName);
if (!isValidEmail(fromEmailAddress)) {
throw new Error(`Invalid sender email address: ${fromEmailAddress}`);
}
if (validateUrls) {
validateURL(successRedirectionURL, 'Success redirect URL');
validateURL(failureRedirectionURL, 'Failure redirect URL');
}
validateCustomVerificationContent(templateContent);
const contentSize = formatContentSize(templateContent);
const sesClient = createSESClient({ accessKeyId, secretAccessKey, region });
if (checkExisting) {
try {
const existingTemplates = await getCustomVerificationTemplates({
accessKeyId,
secretAccessKey,
region,
});
if (existingTemplates.includes(templateName)) {
throw new Error(
`Custom verification template "${templateName}" already exists. Please choose a different name.`
);
}
} catch (error: any) {
if (error.message.includes('already exists')) {
throw error;
}
console.warn('Could not check existing templates:', error);
}
}
const contentLower = templateContent.toLowerCase();
const hasLink =
contentLower.includes('href') || contentLower.includes('link');
const hasVerificationText =
contentLower.includes('verify') ||
contentLower.includes('confirm') ||
contentLower.includes('activate');
if (!hasLink) {
console.warn(
'Template content may be missing verification link - ensure you include proper verification URL in your template'
);
}
if (!hasVerificationText) {
console.warn(
'Template content may be missing verification instructions - consider adding clear verification language'
);
}
const createCommand = new CreateCustomVerificationEmailTemplateCommand({
TemplateName: templateName.trim(),
FromEmailAddress: fromEmailAddress,
TemplateSubject: templateSubject,
TemplateContent: templateContent,
SuccessRedirectionURL: successRedirectionURL.trim(),
FailureRedirectionURL: failureRedirectionURL.trim(),
});
try {
await sesClient.send(createCommand);
return {
success: true,
templateName: templateName.trim(),
message: 'Custom verification email template created successfully',
fromEmailAddress,
templateSubject,
successRedirectionURL: successRedirectionURL.trim(),
failureRedirectionURL: failureRedirectionURL.trim(),
contentSize: contentSize.formatted,
details: {
contentSizeBytes: contentSize.bytes,
subjectLength: templateSubject.length,
contentLength: templateContent.length,
hasVerificationLanguage: hasVerificationText,
hasLinks: hasLink,
},
recommendations: [
...(hasLink
? []
: ['Consider adding verification link in template content']),
...(hasVerificationText
? []
: ['Consider adding clear verification instructions']),
'Test the template with a sample email address',
'Ensure redirect URLs are accessible and provide good user experience',
],
};
} catch (error: any) {
const errorMessage = getCustomVerificationErrorMessage(
error,
templateName
);
throw new Error(errorMessage);
}
},
});

View File

@@ -0,0 +1,181 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { SESClient, CreateTemplateCommand } from '@aws-sdk/client-ses';
import { amazonSesAuth } from '../../index';
import {
createSESClient,
validateTemplateName,
validateTemplateContent,
validateTemplateVariables,
getTemplateErrorMessage,
extractTemplateVariables,
createTemplatePreview,
getEmailTemplates,
} from '../common/ses-utils';
export const createEmailTemplate = createAction({
auth: amazonSesAuth,
name: 'create_email_template',
displayName: 'Create Email Template',
description: 'Create a reusable HTML or text email template with variables',
props: {
templateName: Property.ShortText({
displayName: 'Template Name',
description:
'Unique template name (letters, numbers, underscores, hyphens only)',
required: true,
}),
templateFormat: Property.StaticDropdown({
displayName: 'Template Format',
description: 'Choose template format',
required: true,
defaultValue: 'html',
options: {
options: [
{ label: 'HTML', value: 'html' },
{ label: 'Plain Text', value: 'text' },
{ label: 'Both HTML and Text', value: 'both' },
],
},
}),
subjectPart: Property.ShortText({
displayName: 'Subject',
description: 'Email subject (use {{variable}} for dynamic content)',
required: true,
}),
htmlPart: Property.LongText({
displayName: 'HTML Content',
description: 'HTML email content with variables like {{firstName}}',
required: false,
}),
textPart: Property.LongText({
displayName: 'Text Content',
description: 'Plain text email content with variables like {{firstName}}',
required: false,
}),
checkExisting: Property.Checkbox({
displayName: 'Check if Template Exists',
description: 'Verify template name is unique before creating',
required: false,
defaultValue: true,
}),
sampleData: Property.Object({
displayName: 'Sample Variable Data',
description: 'Test data for template variables (optional preview)',
required: false,
}),
},
async run(context) {
const {
templateName,
templateFormat,
subjectPart,
htmlPart,
textPart,
checkExisting,
sampleData,
} = context.propsValue;
const { accessKeyId, secretAccessKey, region } = context.auth.props;
validateTemplateName(templateName);
if (templateFormat === 'html' && !htmlPart) {
throw new Error('HTML content is required when using HTML format');
}
if (templateFormat === 'text' && !textPart) {
throw new Error('Text content is required when using text format');
}
if (templateFormat === 'both' && (!htmlPart || !textPart)) {
throw new Error(
'Both HTML and text content are required when using both formats'
);
}
validateTemplateContent(htmlPart, textPart);
const templateVariables = validateTemplateVariables(
subjectPart,
htmlPart,
textPart
);
const sesClient = createSESClient({ accessKeyId, secretAccessKey, region });
if (checkExisting) {
try {
const existingTemplates = await getEmailTemplates({
accessKeyId,
secretAccessKey,
region,
});
if (existingTemplates.includes(templateName)) {
throw new Error(
`Template "${templateName}" already exists. Please choose a different name.`
);
}
} catch (error: any) {
if (error.message.includes('already exists')) {
throw error;
}
console.warn('Could not check existing templates:', error);
}
}
const templateData: any = {
TemplateName: templateName.trim(),
SubjectPart: subjectPart,
};
if (templateFormat === 'html' || templateFormat === 'both') {
templateData.HtmlPart = htmlPart;
}
if (templateFormat === 'text' || templateFormat === 'both') {
templateData.TextPart = textPart;
}
const createTemplateCommand = new CreateTemplateCommand({
Template: templateData,
});
try {
await sesClient.send(createTemplateCommand);
let preview: any = {};
if (
sampleData &&
Object.keys(sampleData as Record<string, string>).length > 0
) {
const sampleDataRecord = sampleData as Record<string, string>;
preview = {
subject: createTemplatePreview(subjectPart, sampleDataRecord),
...(htmlPart && {
html: createTemplatePreview(htmlPart, sampleDataRecord),
}),
...(textPart && {
text: createTemplatePreview(textPart, sampleDataRecord),
}),
};
}
return {
success: true,
templateName: templateName.trim(),
message: 'Email template created successfully',
format: templateFormat,
variables: templateVariables,
variableCount: templateVariables.length,
...(Object.keys(preview).length > 0 && { preview }),
details: {
hasHtml: !!htmlPart,
hasText: !!textPart,
subjectLength: subjectPart.length,
htmlLength: htmlPart?.length || 0,
textLength: textPart?.length || 0,
},
};
} catch (error: any) {
const errorMessage = getTemplateErrorMessage(error, templateName);
throw new Error(errorMessage);
}
},
});

View File

@@ -0,0 +1,203 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import {
SESClient,
SendCustomVerificationEmailCommand,
} from '@aws-sdk/client-ses';
import { amazonSesAuth } from '../../index';
import {
getConfigurationSets,
getCustomVerificationTemplates,
createSESClient,
validateEmailAddresses,
getCustomVerificationErrorMessage,
getSESErrorMessage,
createConfigSetDropdownOptions,
isValidEmail,
} from '../common/ses-utils';
export const sendCustomVerificationEmail = createAction({
auth: amazonSesAuth,
name: 'send_custom_verification_email',
displayName: 'Send Custom Verification Email',
description:
'Send verification email to add an email address to SES identities',
props: {
emailAddress: Property.ShortText({
displayName: 'Email Address',
description: 'Email address to verify and add to identities',
required: true,
}),
templateName: Property.Dropdown({
auth: amazonSesAuth,
displayName: 'Verification Template',
description: 'Select custom verification email template',
required: true,
refreshers: [],
options: async ({ auth }) => {
const templates = await getCustomVerificationTemplates(auth as any);
if (templates.length === 0) {
return {
disabled: false,
placeholder:
'No custom verification templates found. Create one first.',
options: [],
};
}
return {
disabled: false,
options: templates.map((template) => ({
label: template,
value: template,
})),
};
},
}),
configurationSetName: Property.Dropdown({
auth: amazonSesAuth,
displayName: 'Configuration Set',
description: 'SES configuration set for tracking (optional)',
required: false,
refreshers: [],
options: async ({ auth }) => {
const configSets = await getConfigurationSets(auth as any);
return createConfigSetDropdownOptions(configSets);
},
}),
validateEmailFormat: Property.Checkbox({
displayName: 'Validate Email Format',
description: 'Check email address format before sending',
required: false,
defaultValue: true,
}),
checkExistingIdentity: Property.Checkbox({
displayName: 'Check if Already Verified',
description: 'Warn if email is already a verified identity',
required: false,
defaultValue: true,
}),
},
async run(context) {
const {
emailAddress,
templateName,
configurationSetName,
validateEmailFormat,
checkExistingIdentity,
} = context.propsValue;
const { accessKeyId, secretAccessKey, region } = context.auth.props;
if (validateEmailFormat) {
const validatedEmails = validateEmailAddresses(
[emailAddress],
'Email address'
);
if (validatedEmails.length === 0) {
throw new Error(`Invalid email address format: ${emailAddress}`);
}
}
const sesClient = createSESClient({ accessKeyId, secretAccessKey, region });
if (checkExistingIdentity) {
try {
const { getVerifiedIdentities } = await import('../common/ses-utils');
const verifiedIdentities = await getVerifiedIdentities({
accessKeyId,
secretAccessKey,
region,
});
if (verifiedIdentities.includes(emailAddress)) {
console.warn(
`Email address ${emailAddress} is already a verified identity`
);
}
} catch (error) {
console.warn('Could not check existing identities:', error);
}
}
const emailDomain = emailAddress.split('@')[1];
const isCommonDomain = [
'gmail.com',
'yahoo.com',
'hotmail.com',
'outlook.com',
].includes(emailDomain?.toLowerCase());
const sendCommand = new SendCustomVerificationEmailCommand({
EmailAddress: emailAddress.trim(),
TemplateName: templateName,
...(configurationSetName &&
configurationSetName.trim() && {
ConfigurationSetName: configurationSetName,
}),
});
try {
const response = await sesClient.send(sendCommand);
return {
success: true,
messageId: response.MessageId,
message: 'Custom verification email sent successfully',
emailAddress: emailAddress.trim(),
templateName,
configurationSetName: configurationSetName || null,
emailDomain,
isCommonDomain,
nextSteps: [
"Check the recipient's email inbox (including spam folder)",
'The recipient should click the verification link in the email',
'Verification status will be updated in AWS SES console',
'You can check verification status using AWS SES API',
],
details: {
emailValidated: validateEmailFormat,
identityChecked: checkExistingIdentity,
hasConfigurationSet: !!configurationSetName,
estimatedDeliveryTime: isCommonDomain
? '1-5 minutes'
: '1-15 minutes',
},
};
} catch (error: any) {
if (
error.name === 'CustomVerificationEmailTemplateDoesNotExistException'
) {
throw new Error(
`Custom verification template "${templateName}" does not exist. Please create the template first or select a different one.`
);
}
if (error.name === 'FromEmailAddressNotVerifiedException') {
throw new Error(
'The sender email address in the verification template is not verified. Please verify the sender address in AWS SES console first.'
);
}
if (error.name === 'ProductionAccessNotGrantedException') {
throw new Error(
'Your AWS SES account is still in sandbox mode. Request production access in the AWS SES console to verify arbitrary email addresses.'
);
}
if (error.name === 'ConfigurationSetDoesNotExistException') {
throw new Error(
`Configuration set "${configurationSetName}" does not exist. Please select a valid configuration set or leave it empty.`
);
}
if (error.name === 'MessageRejected') {
const errorMessage = getSESErrorMessage(error, configurationSetName);
throw new Error(errorMessage);
}
const errorMessage = getCustomVerificationErrorMessage(
error,
templateName
);
throw new Error(errorMessage);
}
},
});

View File

@@ -0,0 +1,259 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { SESClient, SendEmailCommand } from '@aws-sdk/client-ses';
import { amazonSesAuth } from '../../index';
import {
getVerifiedIdentities,
getConfigurationSets,
createSESClient,
validateEmailAddresses,
validateRecipientLimits,
htmlToText,
formatEmailTags,
getSESErrorMessage,
createIdentityDropdownOptions,
createConfigSetDropdownOptions,
isValidEmail,
} from '../common/ses-utils';
export const sendEmail = createAction({
auth: amazonSesAuth,
name: 'send_email',
displayName: 'Send Email',
description:
'Send a customizable email via Amazon SES with verified sender addresses',
props: {
fromEmailAddress: Property.Dropdown({
auth: amazonSesAuth,
displayName: 'From Email',
description: 'Verified sender email address',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please authenticate first',
};
}
const verifiedIdentities = await getVerifiedIdentities(auth.props);
return createIdentityDropdownOptions(verifiedIdentities);
},
}),
toAddresses: Property.Array({
displayName: 'To',
description: 'Recipient email addresses',
required: true,
}),
ccAddresses: Property.Array({
displayName: 'CC',
description: 'Carbon copy recipients',
required: false,
}),
bccAddresses: Property.Array({
displayName: 'BCC',
description: 'Blind carbon copy recipients',
required: false,
}),
subject: Property.ShortText({
displayName: 'Subject',
description: 'Email subject line',
required: true,
}),
bodyFormat: Property.StaticDropdown({
displayName: 'Email Format',
description: 'Choose email format',
required: true,
defaultValue: 'html',
options: {
options: [
{ label: 'HTML', value: 'html' },
{ label: 'Plain Text', value: 'text' },
],
},
}),
htmlBody: Property.LongText({
displayName: 'HTML Content',
description: 'HTML email content (auto-generates text version)',
required: false,
}),
textBody: Property.LongText({
displayName: 'Text Content',
description: 'Plain text email content',
required: false,
}),
replyToAddresses: Property.Array({
displayName: 'Reply To',
description: 'Reply-to email addresses',
required: false,
}),
returnPath: Property.ShortText({
displayName: 'Return Path',
description: 'Email address for bounce notifications',
required: false,
}),
configurationSetName: Property.Dropdown({
auth: amazonSesAuth,
displayName: 'Configuration Set',
description: 'SES configuration set for tracking',
required: false,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please authenticate first',
};
}
const configSets = await getConfigurationSets(auth.props);
return createConfigSetDropdownOptions(configSets);
},
}),
emailTags: Property.Object({
displayName: 'Email Tags',
description: 'Key-value pairs for email tracking and analytics',
required: false,
}),
sourceArn: Property.ShortText({
displayName: 'Source ARN',
description: 'ARN for sending authorization (advanced)',
required: false,
}),
returnPathArn: Property.ShortText({
displayName: 'Return Path ARN',
description: 'ARN for return path authorization (advanced)',
required: false,
}),
},
async run(context) {
const {
fromEmailAddress,
toAddresses,
ccAddresses,
bccAddresses,
subject,
bodyFormat,
htmlBody,
textBody,
replyToAddresses,
returnPath,
configurationSetName,
emailTags,
sourceArn,
returnPathArn,
} = context.propsValue;
const { accessKeyId, secretAccessKey, region } = context.auth.props;
if (bodyFormat === 'html' && !htmlBody) {
throw new Error('HTML content is required when using HTML format');
}
if (bodyFormat === 'text' && !textBody) {
throw new Error('Text content is required when using plain text format');
}
const validatedToAddresses = validateEmailAddresses(
toAddresses as string[],
'To addresses'
);
const validatedCcAddresses = validateEmailAddresses(
ccAddresses as string[],
'CC addresses'
);
const validatedBccAddresses = validateEmailAddresses(
bccAddresses as string[],
'BCC addresses'
);
const validatedReplyToAddresses = validateEmailAddresses(
replyToAddresses as string[],
'Reply-to addresses'
);
validateRecipientLimits(
validatedToAddresses,
validatedCcAddresses,
validatedBccAddresses
);
if (returnPath && !isValidEmail(returnPath)) {
throw new Error(`Invalid return path email: ${returnPath}`);
}
const sesClient = createSESClient({ accessKeyId, secretAccessKey, region });
const emailBody: any = {};
if (bodyFormat === 'html') {
emailBody.Html = {
Charset: 'UTF-8',
Data: htmlBody,
};
emailBody.Text = {
Charset: 'UTF-8',
Data: htmlToText(htmlBody as string),
};
} else {
emailBody.Text = {
Charset: 'UTF-8',
Data: textBody,
};
}
const messageTags = formatEmailTags(emailTags as Record<string, string>);
const sendEmailCommand = new SendEmailCommand({
Source: fromEmailAddress,
Destination: {
ToAddresses: validatedToAddresses,
...(validatedCcAddresses.length > 0 && {
CcAddresses: validatedCcAddresses,
}),
...(validatedBccAddresses.length > 0 && {
BccAddresses: validatedBccAddresses,
}),
},
Message: {
Subject: {
Charset: 'UTF-8',
Data: subject,
},
Body: emailBody,
},
...(validatedReplyToAddresses.length > 0 && {
ReplyToAddresses: validatedReplyToAddresses,
}),
...(returnPath && { ReturnPath: returnPath }),
...(configurationSetName &&
configurationSetName.trim() && {
ConfigurationSetName: configurationSetName,
}),
...(messageTags && { Tags: messageTags }),
...(sourceArn && { SourceArn: sourceArn }),
...(returnPathArn && { ReturnPathArn: returnPathArn }),
});
try {
const response = await sesClient.send(sendEmailCommand);
const totalRecipients =
validatedToAddresses.length +
validatedCcAddresses.length +
validatedBccAddresses.length;
return {
success: true,
messageId: response.MessageId,
message: 'Email sent successfully',
recipientCount: totalRecipients,
format: bodyFormat,
toAddresses: validatedToAddresses,
ccAddresses: validatedCcAddresses,
bccAddresses: validatedBccAddresses,
};
} catch (error: any) {
const errorMessage = getSESErrorMessage(error, configurationSetName);
throw new Error(errorMessage);
}
},
});

View File

@@ -0,0 +1,267 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { SESClient, SendTemplatedEmailCommand } from '@aws-sdk/client-ses';
import { amazonSesAuth } from '../../index';
import {
getVerifiedIdentities,
getConfigurationSets,
getEmailTemplates,
createSESClient,
validateEmailAddresses,
validateRecipientLimits,
formatEmailTags,
getSESErrorMessage,
createIdentityDropdownOptions,
createConfigSetDropdownOptions,
} from '../common/ses-utils';
export const sendTemplatedEmail = createAction({
auth: amazonSesAuth,
name: 'send_templated_email',
displayName: 'Send Templated Email',
description: 'Send personalized emails using pre-created templates',
props: {
fromEmailAddress: Property.Dropdown({
auth: amazonSesAuth,
displayName: 'From Email',
description: 'Verified sender email address',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please authenticate first',
};
}
const verifiedIdentities = await getVerifiedIdentities(auth.props);
return createIdentityDropdownOptions(verifiedIdentities);
},
}),
templateName: Property.Dropdown({
auth: amazonSesAuth,
displayName: 'Email Template',
description: 'Select template to use for this email',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please authenticate first',
};
}
const templates = await getEmailTemplates(auth.props);
if (templates.length === 0) {
return {
disabled: false,
placeholder: 'No templates found. Create a template first.',
options: [],
};
}
return {
disabled: false,
options: templates.map((template) => ({
label: template,
value: template,
})),
};
},
}),
templateData: Property.Object({
displayName: 'Template Variables',
description:
'Data to replace template variables (e.g., {"firstName": "John", "company": "Acme"})',
required: true,
}),
toAddresses: Property.Array({
displayName: 'To',
description: 'Recipient email addresses',
required: true,
}),
ccAddresses: Property.Array({
displayName: 'CC',
description: 'Carbon copy recipients',
required: false,
}),
bccAddresses: Property.Array({
displayName: 'BCC',
description: 'Blind carbon copy recipients',
required: false,
}),
replyToAddresses: Property.Array({
displayName: 'Reply To',
description: 'Reply-to email addresses',
required: false,
}),
returnPath: Property.ShortText({
displayName: 'Return Path',
description: 'Email address for bounce notifications',
required: false,
}),
configurationSetName: Property.Dropdown({
auth: amazonSesAuth,
displayName: 'Configuration Set',
description: 'SES configuration set for tracking',
required: false,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please authenticate first',
};
}
const configSets = await getConfigurationSets(auth.props);
return createConfigSetDropdownOptions(configSets);
},
}),
emailTags: Property.Object({
displayName: 'Email Tags',
description: 'Key-value pairs for email tracking and analytics',
required: false,
}),
sourceArn: Property.ShortText({
displayName: 'Source ARN',
description: 'ARN for sending authorization (advanced)',
required: false,
}),
returnPathArn: Property.ShortText({
displayName: 'Return Path ARN',
description: 'ARN for return path authorization (advanced)',
required: false,
}),
},
async run(context) {
const {
fromEmailAddress,
templateName,
templateData,
toAddresses,
ccAddresses,
bccAddresses,
replyToAddresses,
returnPath,
configurationSetName,
emailTags,
sourceArn,
returnPathArn,
} = context.propsValue;
const { accessKeyId, secretAccessKey, region } = context.auth.props;
if (
!templateData ||
Object.keys(templateData as Record<string, any>).length === 0
) {
throw new Error(
'Template variables are required. Provide at least one key-value pair.'
);
}
const validatedToAddresses = validateEmailAddresses(
toAddresses as string[],
'To addresses'
);
const validatedCcAddresses = validateEmailAddresses(
ccAddresses as string[],
'CC addresses'
);
const validatedBccAddresses = validateEmailAddresses(
bccAddresses as string[],
'BCC addresses'
);
const validatedReplyToAddresses = validateEmailAddresses(
replyToAddresses as string[],
'Reply-to addresses'
);
validateRecipientLimits(
validatedToAddresses,
validatedCcAddresses,
validatedBccAddresses
);
if (
returnPath &&
!validateEmailAddresses([returnPath], 'Return path').length
) {
throw new Error(`Invalid return path email: ${returnPath}`);
}
const sesClient = createSESClient({ accessKeyId, secretAccessKey, region });
let templateDataString: string;
try {
templateDataString = JSON.stringify(templateData);
JSON.parse(templateDataString);
} catch (error) {
throw new Error(
'Template data must be a valid object with key-value pairs'
);
}
const messageTags = formatEmailTags(emailTags as Record<string, string>);
const sendTemplatedEmailCommand = new SendTemplatedEmailCommand({
Source: fromEmailAddress,
Template: templateName,
TemplateData: templateDataString,
Destination: {
ToAddresses: validatedToAddresses,
...(validatedCcAddresses.length > 0 && {
CcAddresses: validatedCcAddresses,
}),
...(validatedBccAddresses.length > 0 && {
BccAddresses: validatedBccAddresses,
}),
},
...(validatedReplyToAddresses.length > 0 && {
ReplyToAddresses: validatedReplyToAddresses,
}),
...(returnPath && { ReturnPath: returnPath }),
...(configurationSetName &&
configurationSetName.trim() && {
ConfigurationSetName: configurationSetName,
}),
...(messageTags && { Tags: messageTags }),
...(sourceArn && { SourceArn: sourceArn }),
...(returnPathArn && { ReturnPathArn: returnPathArn }),
});
try {
const response = await sesClient.send(sendTemplatedEmailCommand);
const totalRecipients =
validatedToAddresses.length +
validatedCcAddresses.length +
validatedBccAddresses.length;
return {
success: true,
messageId: response.MessageId,
message: 'Templated email sent successfully',
templateName,
templateData: JSON.parse(templateDataString),
recipientCount: totalRecipients,
toAddresses: validatedToAddresses,
ccAddresses: validatedCcAddresses,
bccAddresses: validatedBccAddresses,
variablesUsed: Object.keys(templateData as Record<string, any>),
};
} catch (error: any) {
if (error.name === 'TemplateDoesNotExistException') {
throw new Error(
`Template "${templateName}" does not exist. Please create it first or select a different template.`
);
}
const errorMessage = getSESErrorMessage(error, configurationSetName);
throw new Error(errorMessage);
}
},
});

View File

@@ -0,0 +1,268 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import {
SESClient,
UpdateCustomVerificationEmailTemplateCommand,
} from '@aws-sdk/client-ses';
import { amazonSesAuth } from '../../index';
import {
getVerifiedIdentities,
getCustomVerificationTemplates,
getCustomVerificationTemplate,
createSESClient,
validateCustomVerificationTemplateName,
validateURL,
validateCustomVerificationContent,
getCustomVerificationErrorMessage,
formatContentSize,
compareCustomVerificationContent,
createIdentityDropdownOptions,
isValidEmail,
} from '../common/ses-utils';
export const updateCustomVerificationEmailTemplate = createAction({
auth: amazonSesAuth,
name: 'update_custom_verification_email_template',
displayName: 'Update Custom Verification Email Template',
description: 'Modify an existing custom verification email template',
props: {
templateName: Property.Dropdown({
auth: amazonSesAuth,
displayName: 'Template to Update',
description: 'Select custom verification template to modify',
required: true,
refreshers: ['loadCurrentContent'],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please authenticate first',
};
}
const templates = await getCustomVerificationTemplates(auth.props);
if (templates.length === 0) {
return {
disabled: false,
placeholder:
'No custom verification templates found. Create one first.',
options: [],
};
}
return {
disabled: false,
options: templates.map((template) => ({
label: template,
value: template,
})),
};
},
}),
loadCurrentContent: Property.Checkbox({
displayName: 'Load Current Content',
description: 'Pre-fill fields with existing template content',
required: false,
defaultValue: true,
}),
fromEmailAddress: Property.Dropdown({
auth: amazonSesAuth,
displayName: 'From Email',
description: 'Verified sender email address',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please authenticate first',
};
}
const verifiedIdentities = await getVerifiedIdentities(auth.props);
return createIdentityDropdownOptions(verifiedIdentities);
},
}),
templateSubject: Property.ShortText({
displayName: 'Subject',
description: 'Email subject for verification messages',
required: true,
}),
templateContent: Property.LongText({
displayName: 'Email Content',
description:
'HTML content for verification email (must include verification link)',
required: true,
}),
successRedirectionURL: Property.ShortText({
displayName: 'Success Redirect URL',
description: 'URL to redirect users after successful verification',
required: true,
}),
failureRedirectionURL: Property.ShortText({
displayName: 'Failure Redirect URL',
description: 'URL to redirect users if verification fails',
required: true,
}),
preserveUnspecified: Property.Checkbox({
displayName: 'Preserve Unspecified Fields',
description: 'Keep existing values if fields are empty',
required: false,
defaultValue: false,
}),
validateUrls: Property.Checkbox({
displayName: 'Validate URLs',
description: 'Check that redirect URLs are properly formatted',
required: false,
defaultValue: true,
}),
},
async run(context) {
const {
templateName,
fromEmailAddress,
templateSubject,
templateContent,
successRedirectionURL,
failureRedirectionURL,
preserveUnspecified,
validateUrls,
} = context.propsValue;
const { accessKeyId, secretAccessKey, region } = context.auth.props;
validateCustomVerificationTemplateName(templateName);
const sesClient = createSESClient({ accessKeyId, secretAccessKey, region });
let currentTemplate = null;
if (preserveUnspecified) {
currentTemplate = await getCustomVerificationTemplate(
{ accessKeyId, secretAccessKey, region },
templateName
);
if (!currentTemplate) {
throw new Error(
`Custom verification template "${templateName}" does not exist. Cannot update non-existent template.`
);
}
}
const finalFromEmail =
fromEmailAddress || currentTemplate?.fromEmailAddress;
const finalSubject = templateSubject || currentTemplate?.templateSubject;
const finalContent = templateContent || currentTemplate?.templateContent;
const finalSuccessUrl =
successRedirectionURL || currentTemplate?.successRedirectionURL;
const finalFailureUrl =
failureRedirectionURL || currentTemplate?.failureRedirectionURL;
if (!finalFromEmail) {
throw new Error('From email address is required');
}
if (!finalSubject) {
throw new Error('Subject is required');
}
if (!finalContent) {
throw new Error('Template content is required');
}
if (!finalSuccessUrl) {
throw new Error('Success redirect URL is required');
}
if (!finalFailureUrl) {
throw new Error('Failure redirect URL is required');
}
if (!isValidEmail(finalFromEmail)) {
throw new Error(`Invalid sender email address: ${finalFromEmail}`);
}
if (validateUrls) {
validateURL(finalSuccessUrl, 'Success redirect URL');
validateURL(finalFailureUrl, 'Failure redirect URL');
}
validateCustomVerificationContent(finalContent);
const contentSize = formatContentSize(finalContent);
const contentLower = finalContent.toLowerCase();
const hasLink =
contentLower.includes('href') || contentLower.includes('link');
const hasVerificationText =
contentLower.includes('verify') ||
contentLower.includes('confirm') ||
contentLower.includes('activate');
const updateCommand = new UpdateCustomVerificationEmailTemplateCommand({
TemplateName: templateName,
FromEmailAddress: finalFromEmail,
TemplateSubject: finalSubject,
TemplateContent: finalContent,
SuccessRedirectionURL: finalSuccessUrl.trim(),
FailureRedirectionURL: finalFailureUrl.trim(),
});
try {
await sesClient.send(updateCommand);
let changes: any = {};
if (currentTemplate) {
changes = compareCustomVerificationContent(
{
fromEmailAddress: currentTemplate.fromEmailAddress,
templateSubject: currentTemplate.templateSubject,
templateContent: currentTemplate.templateContent,
successRedirectionURL: currentTemplate.successRedirectionURL,
failureRedirectionURL: currentTemplate.failureRedirectionURL,
},
{
fromEmailAddress: finalFromEmail,
templateSubject: finalSubject,
templateContent: finalContent,
successRedirectionURL: finalSuccessUrl,
failureRedirectionURL: finalFailureUrl,
}
);
}
return {
success: true,
templateName,
message: 'Custom verification email template updated successfully',
fromEmailAddress: finalFromEmail,
templateSubject: finalSubject,
successRedirectionURL: finalSuccessUrl.trim(),
failureRedirectionURL: finalFailureUrl.trim(),
contentSize: contentSize.formatted,
...(Object.keys(changes).length > 0 && { changes }),
details: {
contentSizeBytes: contentSize.bytes,
subjectLength: finalSubject.length,
contentLength: finalContent.length,
hasVerificationLanguage: hasVerificationText,
hasLinks: hasLink,
preservedContent: preserveUnspecified,
},
recommendations: [
...(hasLink
? []
: ['Consider adding verification link in template content']),
...(hasVerificationText
? []
: ['Consider adding clear verification instructions']),
'Test the updated template with a sample email address',
'Ensure redirect URLs are accessible and provide good user experience',
'Verify that the sender email address is still verified in SES',
],
};
} catch (error: any) {
const errorMessage = getCustomVerificationErrorMessage(
error,
templateName
);
throw new Error(errorMessage);
}
},
});

View File

@@ -0,0 +1,233 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { SESClient, UpdateTemplateCommand } from '@aws-sdk/client-ses';
import { amazonSesAuth } from '../../index';
import {
getEmailTemplates,
getEmailTemplate,
createSESClient,
validateTemplateContent,
validateTemplateVariables,
getTemplateErrorMessage,
extractTemplateVariables,
createTemplatePreview,
compareTemplateContent,
} from '../common/ses-utils';
export const updateEmailTemplate = createAction({
auth: amazonSesAuth,
name: 'update_email_template',
displayName: 'Update Email Template',
description: 'Modify an existing email template with new content',
props: {
templateName: Property.Dropdown({
auth: amazonSesAuth,
displayName: 'Template to Update',
description: 'Select template to modify',
required: true,
refreshers: ['loadCurrentContent'],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please authenticate first',
};
}
const templates = await getEmailTemplates(auth.props);
if (templates.length === 0) {
return {
disabled: false,
placeholder: 'No templates found. Create a template first.',
options: [],
};
}
return {
disabled: false,
options: templates.map((template) => ({
label: template,
value: template,
})),
};
},
}),
loadCurrentContent: Property.Checkbox({
displayName: 'Load Current Content',
description: 'Pre-fill fields with existing template content',
required: false,
defaultValue: true,
}),
templateFormat: Property.StaticDropdown({
displayName: 'Template Format',
description: 'Choose template format',
required: true,
defaultValue: 'html',
options: {
options: [
{ label: 'HTML', value: 'html' },
{ label: 'Plain Text', value: 'text' },
{ label: 'Both HTML and Text', value: 'both' },
],
},
}),
subjectPart: Property.ShortText({
displayName: 'Subject',
description: 'Email subject (use {{variable}} for dynamic content)',
required: true,
}),
htmlPart: Property.LongText({
displayName: 'HTML Content',
description: 'HTML email content with variables like {{firstName}}',
required: false,
}),
textPart: Property.LongText({
displayName: 'Text Content',
description: 'Plain text email content with variables like {{firstName}}',
required: false,
}),
preserveUnspecified: Property.Checkbox({
displayName: 'Preserve Unspecified Content',
description: 'Keep existing HTML/text content if not provided',
required: false,
defaultValue: false,
}),
sampleData: Property.Object({
displayName: 'Sample Variable Data',
description: 'Test data for template variables (optional preview)',
required: false,
}),
},
async run(context) {
const {
templateName,
templateFormat,
subjectPart,
htmlPart,
textPart,
preserveUnspecified,
sampleData,
} = context.propsValue;
const { accessKeyId, secretAccessKey, region } = context.auth.props;
const sesClient = createSESClient({ accessKeyId, secretAccessKey, region });
let currentTemplate = null;
if (preserveUnspecified) {
currentTemplate = await getEmailTemplate(
{ accessKeyId, secretAccessKey, region },
templateName
);
if (!currentTemplate) {
throw new Error(
`Template "${templateName}" does not exist. Cannot update non-existent template.`
);
}
}
let finalHtmlPart = htmlPart;
let finalTextPart = textPart;
if (preserveUnspecified && currentTemplate) {
finalHtmlPart = htmlPart || currentTemplate.htmlPart;
finalTextPart = textPart || currentTemplate.textPart;
}
if (templateFormat === 'html' && !finalHtmlPart) {
throw new Error('HTML content is required when using HTML format');
}
if (templateFormat === 'text' && !finalTextPart) {
throw new Error('Text content is required when using text format');
}
if (templateFormat === 'both' && (!finalHtmlPart || !finalTextPart)) {
throw new Error(
'Both HTML and text content are required when using both formats'
);
}
validateTemplateContent(finalHtmlPart, finalTextPart);
const templateVariables = validateTemplateVariables(
subjectPart,
finalHtmlPart,
finalTextPart
);
const templateData: any = {
TemplateName: templateName,
SubjectPart: subjectPart,
};
if (templateFormat === 'html' || templateFormat === 'both') {
templateData.HtmlPart = finalHtmlPart;
}
if (templateFormat === 'text' || templateFormat === 'both') {
templateData.TextPart = finalTextPart;
}
const updateTemplateCommand = new UpdateTemplateCommand({
Template: templateData,
});
try {
await sesClient.send(updateTemplateCommand);
let preview: any = {};
if (
sampleData &&
Object.keys(sampleData as Record<string, string>).length > 0
) {
const sampleDataRecord = sampleData as Record<string, string>;
preview = {
subject: createTemplatePreview(subjectPart, sampleDataRecord),
...(finalHtmlPart && {
html: createTemplatePreview(finalHtmlPart, sampleDataRecord),
}),
...(finalTextPart && {
text: createTemplatePreview(finalTextPart, sampleDataRecord),
}),
};
}
let changes: any = {};
if (currentTemplate) {
changes = compareTemplateContent(
{
subjectPart: currentTemplate.subjectPart,
htmlPart: currentTemplate.htmlPart,
textPart: currentTemplate.textPart,
},
{
subjectPart,
htmlPart: finalHtmlPart,
textPart: finalTextPart,
}
);
}
return {
success: true,
templateName,
message: 'Email template updated successfully',
format: templateFormat,
variables: templateVariables,
variableCount: templateVariables.length,
...(Object.keys(preview).length > 0 && { preview }),
...(Object.keys(changes).length > 0 && { changes }),
details: {
hasHtml: !!finalHtmlPart,
hasText: !!finalTextPart,
subjectLength: subjectPart.length,
htmlLength: finalHtmlPart?.length || 0,
textLength: finalTextPart?.length || 0,
preservedContent: preserveUnspecified,
},
};
} catch (error: any) {
const errorMessage = getTemplateErrorMessage(error, templateName);
throw new Error(errorMessage);
}
},
});

View File

@@ -0,0 +1,745 @@
import {
SESClient,
ListIdentitiesCommand,
ListConfigurationSetsCommand,
GetIdentityVerificationAttributesCommand,
ListTemplatesCommand,
GetTemplateCommand,
ListCustomVerificationEmailTemplatesCommand,
GetCustomVerificationEmailTemplateCommand,
} from '@aws-sdk/client-ses';
export interface SESAuth {
accessKeyId: string;
secretAccessKey: string;
region: string;
}
/**
* Creates a configured SES client
*/
export function createSESClient(auth: SESAuth): SESClient {
return new SESClient({
credentials: {
accessKeyId: auth.accessKeyId,
secretAccessKey: auth.secretAccessKey,
},
region: auth.region,
});
}
/**
* Fetches and filters verified identities from AWS SES
*/
export async function getVerifiedIdentities(auth: SESAuth): Promise<string[]> {
const sesClient = createSESClient(auth);
try {
// Get all identities
const identitiesResponse = await sesClient.send(
new ListIdentitiesCommand({})
);
const identities = identitiesResponse.Identities || [];
if (identities.length === 0) {
return [];
}
// Check verification status for all identities
const verificationResponse = await sesClient.send(
new GetIdentityVerificationAttributesCommand({
Identities: identities,
})
);
// Filter to only verified identities
const verifiedIdentities = identities.filter(
(identity) =>
verificationResponse.VerificationAttributes?.[identity]
?.VerificationStatus === 'Success'
);
return verifiedIdentities;
} catch (error) {
console.warn('Failed to fetch verified identities:', error);
return [];
}
}
/**
* Fetches configuration sets from AWS SES
*/
export async function getConfigurationSets(auth: SESAuth): Promise<string[]> {
const sesClient = createSESClient(auth);
try {
const response = await sesClient.send(new ListConfigurationSetsCommand({}));
return (
response.ConfigurationSets?.map((cs) => cs.Name).filter(
(name): name is string => !!name
) || []
);
} catch (error) {
console.warn('Failed to fetch configuration sets:', error);
return [];
}
}
/**
* Validates email address format
*/
export function isValidEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email.trim());
}
/**
* Converts HTML content to plain text
* Strips HTML tags and converts common elements to text equivalents
*/
export function htmlToText(html: string): string {
return html
.replace(/<br\s*\/?>/gi, '\n')
.replace(/<\/p>/gi, '\n\n')
.replace(/<\/div>/gi, '\n')
.replace(/<\/h[1-6]>/gi, '\n\n')
.replace(/<li>/gi, '• ')
.replace(/<\/li>/gi, '\n')
.replace(/<[^>]*>/g, '')
.replace(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/\n\s*\n\s*\n/g, '\n\n') // Remove excessive line breaks
.trim();
}
/**
* Validates and sanitizes email addresses from arrays
*/
export function validateEmailAddresses(
emails: string[] | string | undefined,
fieldName: string
): string[] {
if (!emails) return [];
const emailArray = Array.isArray(emails) ? emails : [emails];
const validEmails: string[] = [];
const invalidEmails: string[] = [];
emailArray.forEach((email) => {
const trimmedEmail = email.trim();
if (trimmedEmail) {
if (isValidEmail(trimmedEmail)) {
validEmails.push(trimmedEmail);
} else {
invalidEmails.push(trimmedEmail);
}
}
});
if (invalidEmails.length > 0) {
throw new Error(
`Invalid email addresses in ${fieldName}: ${invalidEmails.join(', ')}`
);
}
return validEmails;
}
/**
* Checks AWS SES recipient limits
*/
export function validateRecipientLimits(
toAddresses: string[],
ccAddresses: string[] = [],
bccAddresses: string[] = []
): void {
const totalRecipients =
toAddresses.length + ccAddresses.length + bccAddresses.length;
if (totalRecipients === 0) {
throw new Error('At least one recipient is required (To, CC, or BCC)');
}
if (totalRecipients > 50) {
throw new Error(
`Too many recipients (${totalRecipients}). AWS SES allows maximum 50 recipients per email.`
);
}
}
/**
* Converts email tags object to AWS SES MessageTag format
*/
export function formatEmailTags(
tags: Record<string, string> | undefined
): Array<{ Name: string; Value: string }> | undefined {
if (!tags || Object.keys(tags).length === 0) {
return undefined;
}
return Object.entries(tags).map(([key, value]) => ({
Name: key.trim(),
Value: String(value).trim(),
}));
}
/**
* Gets user-friendly error message for AWS SES errors
*/
export function getSESErrorMessage(
error: any,
configurationSetName?: string
): string {
switch (error.name) {
case 'MessageRejected':
return `Email rejected: ${error.message}`;
case 'AccountSendingPausedException':
return 'Email sending is disabled for your AWS account. Contact AWS support to enable it.';
case 'ConfigurationSetDoesNotExistException':
return `Configuration set "${configurationSetName}" does not exist.`;
case 'ConfigurationSetSendingPausedException':
return `Email sending is disabled for configuration set "${configurationSetName}".`;
case 'MailFromDomainNotVerifiedException':
return 'The custom MAIL FROM domain is not verified. Please verify it in the AWS SES console.';
case 'InvalidParameterValue':
return `Invalid parameter: ${error.message}`;
case 'ThrottlingException':
return 'Request was throttled. Please retry after a moment.';
default:
return `Failed to send email: ${
error.message || 'Unknown error occurred'
}`;
}
}
/**
* Dropdown option type for Activepieces
*/
export interface DropdownOption {
label: string;
value: string;
}
/**
* Creates dropdown options from verified identities
*/
export function createIdentityDropdownOptions(identities: string[]): {
disabled: boolean;
placeholder?: string;
options: DropdownOption[];
} {
if (identities.length === 0) {
return {
disabled: false,
placeholder:
'No verified identities found. Please verify an email address or domain in AWS SES console.',
options: [],
};
}
return {
disabled: false,
options: identities.map((identity) => ({
label: identity,
value: identity,
})),
};
}
/**
* Creates dropdown options from configuration sets
*/
export function createConfigSetDropdownOptions(configSets: string[]): {
disabled: boolean;
options: DropdownOption[];
} {
return {
disabled: false,
options: [
{ label: 'None', value: '' },
...configSets.map((name) => ({
label: name,
value: name,
})),
],
};
}
/**
* Fetches existing email templates from AWS SES
*/
export async function getEmailTemplates(auth: SESAuth): Promise<string[]> {
const sesClient = createSESClient(auth);
try {
const response = await sesClient.send(new ListTemplatesCommand({}));
return (
response.TemplatesMetadata?.map((template) => template.Name).filter(
(name): name is string => !!name
) || []
);
} catch (error) {
console.warn('Failed to fetch email templates:', error);
return [];
}
}
/**
* Fetches a specific email template from AWS SES
*/
export async function getEmailTemplate(
auth: SESAuth,
templateName: string
): Promise<{
templateName: string;
subjectPart?: string;
htmlPart?: string;
textPart?: string;
} | null> {
const sesClient = createSESClient(auth);
try {
const response = await sesClient.send(
new GetTemplateCommand({
TemplateName: templateName,
})
);
if (response.Template) {
return {
templateName: response.Template.TemplateName || templateName,
subjectPart: response.Template.SubjectPart,
htmlPart: response.Template.HtmlPart,
textPart: response.Template.TextPart,
};
}
return null;
} catch (error: any) {
if (error.name === 'TemplateDoesNotExistException') {
return null;
}
console.warn('Failed to fetch email template:', error);
return null;
}
}
/**
* Validates template name format
*/
export function validateTemplateName(name: string): void {
if (!name || name.trim().length === 0) {
throw new Error('Template name is required');
}
// AWS SES template name requirements
const trimmedName = name.trim();
if (trimmedName.length > 64) {
throw new Error('Template name must be 64 characters or less');
}
// Template name can only contain alphanumeric characters, underscores, and hyphens
const validNameRegex = /^[a-zA-Z0-9_-]+$/;
if (!validNameRegex.test(trimmedName)) {
throw new Error(
'Template name can only contain letters, numbers, underscores, and hyphens'
);
}
}
/**
* Validates template content
*/
export function validateTemplateContent(
htmlPart?: string,
textPart?: string
): void {
if (!htmlPart && !textPart) {
throw new Error('At least one of HTML or text content must be provided');
}
// Check content size limits (AWS SES limits)
if (htmlPart && htmlPart.length > 500000) {
throw new Error('HTML content must be 500KB or less');
}
if (textPart && textPart.length > 500000) {
throw new Error('Text content must be 500KB or less');
}
}
/**
* Gets user-friendly error message for template-related AWS SES errors
*/
export function getTemplateErrorMessage(
error: any,
templateName?: string
): string {
switch (error.name) {
case 'AlreadyExistsException':
return `Template "${templateName}" already exists. Please choose a different name.`;
case 'InvalidTemplateException':
return 'Template content is invalid. Please check your template syntax and variables.';
case 'LimitExceededException':
return 'You have reached the maximum number of email templates allowed for your account.';
case 'TemplateDoesNotExistException':
return `Template "${templateName}" does not exist.`;
case 'ThrottlingException':
return 'Request was throttled. Please retry after a moment.';
default:
return `Failed to process template: ${
error.message || 'Unknown error occurred'
}`;
}
}
/**
* Extracts and validates template variables from content
*/
export function extractTemplateVariables(content: string): string[] {
const variableRegex = /\{\{([^}]+)\}\}/g;
const variables: string[] = [];
let match;
while ((match = variableRegex.exec(content)) !== null) {
const variable = match[1].trim();
if (variable && !variables.includes(variable)) {
variables.push(variable);
}
}
return variables;
}
/**
* Validates template variable syntax
*/
export function validateTemplateVariables(
subject: string,
htmlPart?: string,
textPart?: string
): string[] {
const allVariables: string[] = [];
// Extract variables from all content parts
allVariables.push(...extractTemplateVariables(subject));
if (htmlPart) {
allVariables.push(...extractTemplateVariables(htmlPart));
}
if (textPart) {
allVariables.push(...extractTemplateVariables(textPart));
}
// Remove duplicates
const uniqueVariables = [...new Set(allVariables)];
// Validate variable names
const invalidVariables = uniqueVariables.filter((variable) => {
// AWS SES template variables should not contain spaces or special characters except dots
return !/^[a-zA-Z0-9_.]+$/.test(variable);
});
if (invalidVariables.length > 0) {
throw new Error(
`Invalid template variables: ${invalidVariables.join(
', '
)}. Variables can only contain letters, numbers, underscores, and dots.`
);
}
return uniqueVariables;
}
/**
* Creates template preview with sample data
*/
export function createTemplatePreview(
content: string,
sampleData: Record<string, string> = {}
): string {
let preview = content;
// Replace template variables with sample data
const variables = extractTemplateVariables(content);
variables.forEach((variable) => {
const value = sampleData[variable] || `[${variable}]`;
const regex = new RegExp(`\\{\\{\\s*${variable}\\s*\\}\\}`, 'g');
preview = preview.replace(regex, value);
});
return preview;
}
/**
* Compares two template versions and returns what changed
*/
export function compareTemplateContent(
current: {
subjectPart?: string;
htmlPart?: string;
textPart?: string;
},
updated: {
subjectPart: string;
htmlPart?: string;
textPart?: string;
}
): {
subjectChanged: boolean;
htmlChanged: boolean;
textChanged: boolean;
summary: string[];
} {
const changes = {
subjectChanged: current.subjectPart !== updated.subjectPart,
htmlChanged: current.htmlPart !== updated.htmlPart,
textChanged: current.textPart !== updated.textPart,
};
const summary: string[] = [];
if (changes.subjectChanged) {
summary.push('Subject updated');
}
if (changes.htmlChanged) {
if (current.htmlPart && updated.htmlPart) {
summary.push('HTML content modified');
} else if (!current.htmlPart && updated.htmlPart) {
summary.push('HTML content added');
} else if (current.htmlPart && !updated.htmlPart) {
summary.push('HTML content removed');
}
}
if (changes.textChanged) {
if (current.textPart && updated.textPart) {
summary.push('Text content modified');
} else if (!current.textPart && updated.textPart) {
summary.push('Text content added');
} else if (current.textPart && !updated.textPart) {
summary.push('Text content removed');
}
}
if (summary.length === 0) {
summary.push('No changes detected');
}
return { ...changes, summary };
}
/**
* Fetches existing custom verification email templates from AWS SES
*/
export async function getCustomVerificationTemplates(
auth: SESAuth
): Promise<string[]> {
const sesClient = createSESClient(auth);
try {
const response = await sesClient.send(
new ListCustomVerificationEmailTemplatesCommand({})
);
return (
response.CustomVerificationEmailTemplates?.map(
(template) => template.TemplateName
).filter((name): name is string => !!name) || []
);
} catch (error) {
console.warn('Failed to fetch custom verification templates:', error);
return [];
}
}
/**
* Fetches a specific custom verification email template from AWS SES
*/
export async function getCustomVerificationTemplate(
auth: SESAuth,
templateName: string
): Promise<{
templateName: string;
fromEmailAddress?: string;
templateSubject?: string;
templateContent?: string;
successRedirectionURL?: string;
failureRedirectionURL?: string;
} | null> {
const sesClient = createSESClient(auth);
try {
const response = await sesClient.send(
new GetCustomVerificationEmailTemplateCommand({
TemplateName: templateName,
})
);
return {
templateName: response.TemplateName || templateName,
fromEmailAddress: response.FromEmailAddress,
templateSubject: response.TemplateSubject,
templateContent: response.TemplateContent,
successRedirectionURL: response.SuccessRedirectionURL,
failureRedirectionURL: response.FailureRedirectionURL,
};
} catch (error: any) {
if (error.name === 'CustomVerificationEmailTemplateDoesNotExistException') {
return null;
}
console.warn('Failed to fetch custom verification template:', error);
return null;
}
}
/**
* Validates custom verification template name format
*/
export function validateCustomVerificationTemplateName(name: string): void {
validateTemplateName(name); // Uses same rules as regular templates
}
/**
* Validates URL format
*/
export function validateURL(url: string, fieldName: string): void {
if (!url || url.trim().length === 0) {
throw new Error(`${fieldName} is required`);
}
try {
new URL(url.trim());
} catch (error) {
throw new Error(
`${fieldName} must be a valid URL (e.g., https://example.com)`
);
}
}
/**
* Validates custom verification template content
*/
export function validateCustomVerificationContent(content: string): void {
if (!content || content.trim().length === 0) {
throw new Error('Template content is required');
}
// Check content size limits (AWS SES limit is 10MB)
const contentSizeBytes = new TextEncoder().encode(content).length;
const maxSizeBytes = 10 * 1024 * 1024; // 10 MB
if (contentSizeBytes > maxSizeBytes) {
throw new Error(
`Template content size (${Math.round(
contentSizeBytes / 1024 / 1024
)}MB) exceeds the 10MB limit`
);
}
}
/**
* Compares two custom verification template versions and returns what changed
*/
export function compareCustomVerificationContent(
current: {
fromEmailAddress?: string;
templateSubject?: string;
templateContent?: string;
successRedirectionURL?: string;
failureRedirectionURL?: string;
},
updated: {
fromEmailAddress: string;
templateSubject: string;
templateContent: string;
successRedirectionURL: string;
failureRedirectionURL: string;
}
): {
fromEmailChanged: boolean;
subjectChanged: boolean;
contentChanged: boolean;
successUrlChanged: boolean;
failureUrlChanged: boolean;
summary: string[];
} {
const changes = {
fromEmailChanged: current.fromEmailAddress !== updated.fromEmailAddress,
subjectChanged: current.templateSubject !== updated.templateSubject,
contentChanged: current.templateContent !== updated.templateContent,
successUrlChanged:
current.successRedirectionURL !== updated.successRedirectionURL,
failureUrlChanged:
current.failureRedirectionURL !== updated.failureRedirectionURL,
};
const summary: string[] = [];
if (changes.fromEmailChanged) {
summary.push('Sender email address updated');
}
if (changes.subjectChanged) {
summary.push('Subject line updated');
}
if (changes.contentChanged) {
summary.push('Template content modified');
}
if (changes.successUrlChanged) {
summary.push('Success redirect URL updated');
}
if (changes.failureUrlChanged) {
summary.push('Failure redirect URL updated');
}
if (summary.length === 0) {
summary.push('No changes detected');
}
return { ...changes, summary };
}
/**
* Gets user-friendly error message for custom verification template errors
*/
export function getCustomVerificationErrorMessage(
error: any,
templateName?: string
): string {
switch (error.name) {
case 'CustomVerificationEmailTemplateAlreadyExistsException':
return `Custom verification template "${templateName}" already exists. Please choose a different name.`;
case 'CustomVerificationEmailInvalidContentException':
return 'Template content is invalid. Please check your HTML content and ensure it meets AWS SES requirements.';
case 'FromEmailAddressNotVerifiedException':
return 'The sender email address is not verified. Please verify the email address in AWS SES console first.';
case 'LimitExceededException':
return 'You have reached the maximum number of custom verification templates allowed for your account.';
case 'ThrottlingException':
return 'Request was throttled. Please retry after a moment.';
default:
return `Failed to process custom verification template: ${
error.message || 'Unknown error occurred'
}`;
}
}
/**
* Calculates and formats content size
*/
export function formatContentSize(content: string): {
bytes: number;
formatted: string;
} {
const bytes = new TextEncoder().encode(content).length;
if (bytes < 1024) {
return { bytes, formatted: `${bytes} bytes` };
} else if (bytes < 1024 * 1024) {
return { bytes, formatted: `${Math.round(bytes / 1024)}KB` };
} else {
return {
bytes,
formatted: `${Math.round((bytes / 1024 / 1024) * 10) / 10}MB`,
};
}
}

View File

@@ -0,0 +1,19 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"forceConsistentCasingInFileNames": true,
"strict": true,
"importHelpers": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noPropertyAccessFromIndexSignature": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}

View File

@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../../dist/out-tsc",
"declaration": true,
"types": ["node"]
},
"include": ["src/**/*.ts"]
}