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:
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../.eslintrc.base.json"
|
||||
],
|
||||
"ignorePatterns": [
|
||||
"!**/*"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"*.ts",
|
||||
"*.tsx",
|
||||
"*.js",
|
||||
"*.jsx"
|
||||
],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.ts",
|
||||
"*.tsx"
|
||||
],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.js",
|
||||
"*.jsx"
|
||||
],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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.
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "@activepieces/piece-amazon-ses",
|
||||
"version": "0.0.3",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-ses": "3.864.0"
|
||||
}
|
||||
}
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -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: [],
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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(/ /g, ' ')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/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`,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user