diff --git a/packages/backend/src/apps/mailchimp/actions/create-campaign/index.js b/packages/backend/src/apps/mailchimp/actions/create-campaign/index.js
new file mode 100644
index 00000000..f622e150
--- /dev/null
+++ b/packages/backend/src/apps/mailchimp/actions/create-campaign/index.js
@@ -0,0 +1,180 @@
+import defineAction from '../../../../helpers/define-action.js';
+
+export default defineAction({
+ name: 'Create campaign',
+ key: 'createCampaign',
+ description: 'Creates a new campaign draft.',
+ arguments: [
+ {
+ label: 'Campaign Name',
+ key: 'campaignName',
+ type: 'string',
+ required: false,
+ description: '',
+ variables: true,
+ },
+ {
+ label: 'Audience',
+ key: 'audienceId',
+ type: 'dropdown',
+ required: true,
+ description: '',
+ variables: true,
+ source: {
+ type: 'query',
+ name: 'getDynamicData',
+ arguments: [
+ {
+ name: 'key',
+ value: 'listAudiences',
+ },
+ ],
+ },
+ },
+ {
+ label: 'Segment or Tag',
+ key: 'segmentOrTagId',
+ type: 'dropdown',
+ required: false,
+ dependsOn: ['parameters.audienceId'],
+ description:
+ 'Choose the specific segment or tag to which you"d like to direct the campaign. If no segment or tag is chosen, the campaign will be distributed to the entire audience previously selected.',
+ variables: true,
+ source: {
+ type: 'query',
+ name: 'getDynamicData',
+ arguments: [
+ {
+ name: 'key',
+ value: 'listSegmentsOrTags',
+ },
+ {
+ name: 'parameters.audienceId',
+ value: '{parameters.audienceId}',
+ },
+ ],
+ },
+ },
+ {
+ label: 'Email Subject',
+ key: 'emailSubject',
+ type: 'string',
+ required: true,
+ description: '',
+ variables: true,
+ },
+ {
+ label: 'Preview Text',
+ key: 'previewText',
+ type: 'string',
+ required: false,
+ description:
+ 'The snippet will be visible in the inbox following the subject line.',
+ variables: true,
+ },
+ {
+ label: 'From Name',
+ key: 'fromName',
+ type: 'string',
+ required: true,
+ description: 'The "from" name on the campaign (not an email address).',
+ variables: true,
+ },
+ {
+ label: 'From Email Address',
+ key: 'fromEmailAddress',
+ type: 'string',
+ required: true,
+ description: 'The reply-to email address for the campaign.',
+ variables: true,
+ },
+ {
+ label: 'To Name',
+ key: 'toName',
+ type: 'string',
+ required: false,
+ description:
+ 'Supports *|MERGETAGS|* for recipient name, such as *|FNAME|*, *|LNAME|*, *|FNAME|* *|LNAME|*, etc.',
+ variables: true,
+ },
+ {
+ label: 'Template',
+ key: 'templateId',
+ type: 'dropdown',
+ required: false,
+ description:
+ 'Select either a template or provide HTML email content, you cannot provide both. If both fields are left blank, the campaign draft will have no content.',
+ variables: true,
+ source: {
+ type: 'query',
+ name: 'getDynamicData',
+ arguments: [
+ {
+ name: 'key',
+ value: 'listTemplates',
+ },
+ ],
+ },
+ },
+ {
+ label: 'Email Content (HTML)',
+ key: 'emailContent',
+ type: 'string',
+ required: false,
+ description:
+ 'Select either a template or provide HTML email content, you cannot provide both. If both fields are left blank, the campaign draft will have no content.',
+ variables: true,
+ },
+ ],
+
+ async run($) {
+ const {
+ campaignName,
+ audienceId,
+ segmentOrTagId,
+ emailSubject,
+ previewText,
+ fromName,
+ fromEmailAddress,
+ toName,
+ templateId,
+ emailContent,
+ } = $.step.parameters;
+
+ const body = {
+ type: 'regular',
+ recipients: {
+ list_id: audienceId,
+ segment_opts: {
+ saved_segment_id: Number(segmentOrTagId),
+ },
+ },
+ settings: {
+ subject_line: emailSubject,
+ reply_to: fromEmailAddress,
+ title: campaignName,
+ preview_text: previewText,
+ from_name: fromName,
+ to_name: toName,
+ },
+ };
+
+ const { data: campaign } = await $.http.post('/3.0/campaigns', body);
+
+ const campaignBody = {
+ template: {
+ id: Number(templateId),
+ },
+ html: emailContent,
+ };
+
+ const { data } = await $.http.put(
+ `/3.0/campaigns/${campaign.id}/content`,
+ campaignBody
+ );
+
+ $.setActionItem({
+ raw: data,
+ });
+ },
+});
diff --git a/packages/backend/src/apps/mailchimp/actions/index.js b/packages/backend/src/apps/mailchimp/actions/index.js
new file mode 100644
index 00000000..853ccb5a
--- /dev/null
+++ b/packages/backend/src/apps/mailchimp/actions/index.js
@@ -0,0 +1,4 @@
+import createCampaign from './create-campaign/index.js';
+import sendCampaign from './send-campaign/index.js';
+
+export default [createCampaign, sendCampaign];
diff --git a/packages/backend/src/apps/mailchimp/actions/send-campaign/index.js b/packages/backend/src/apps/mailchimp/actions/send-campaign/index.js
new file mode 100644
index 00000000..1c954cb9
--- /dev/null
+++ b/packages/backend/src/apps/mailchimp/actions/send-campaign/index.js
@@ -0,0 +1,39 @@
+import defineAction from '../../../../helpers/define-action.js';
+
+export default defineAction({
+ name: 'Send campaign',
+ key: 'sendCampaign',
+ description: 'Sends a campaign draft.',
+ arguments: [
+ {
+ label: 'Campaign',
+ key: 'campaignId',
+ type: 'dropdown',
+ required: true,
+ description: '',
+ variables: true,
+ source: {
+ type: 'query',
+ name: 'getDynamicData',
+ arguments: [
+ {
+ name: 'key',
+ value: 'listCampaigns',
+ },
+ ],
+ },
+ },
+ ],
+
+ async run($) {
+ const campaignId = $.step.parameters.campaignId;
+
+ await $.http.post(`/3.0/campaigns/${campaignId}/actions/send`);
+
+ $.setActionItem({
+ raw: {
+ output: 'sent',
+ },
+ });
+ },
+});
diff --git a/packages/backend/src/apps/mailchimp/assets/favicon.svg b/packages/backend/src/apps/mailchimp/assets/favicon.svg
new file mode 100644
index 00000000..55af85db
--- /dev/null
+++ b/packages/backend/src/apps/mailchimp/assets/favicon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/backend/src/apps/mailchimp/auth/generate-auth-url.js b/packages/backend/src/apps/mailchimp/auth/generate-auth-url.js
new file mode 100644
index 00000000..380fe61d
--- /dev/null
+++ b/packages/backend/src/apps/mailchimp/auth/generate-auth-url.js
@@ -0,0 +1,19 @@
+import { URLSearchParams } from 'url';
+
+export default async function generateAuthUrl($) {
+ const oauthRedirectUrlField = $.app.auth.fields.find(
+ (field) => field.key == 'oAuthRedirectUrl'
+ );
+ const redirectUri = oauthRedirectUrlField.value;
+ const searchParams = new URLSearchParams({
+ response_type: 'code',
+ client_id: $.auth.data.clientId,
+ redirect_uri: redirectUri,
+ });
+
+ const url = `https://login.mailchimp.com/oauth2/authorize?${searchParams.toString()}`;
+
+ await $.auth.set({
+ url,
+ });
+}
diff --git a/packages/backend/src/apps/mailchimp/auth/index.js b/packages/backend/src/apps/mailchimp/auth/index.js
new file mode 100644
index 00000000..d69cfd43
--- /dev/null
+++ b/packages/backend/src/apps/mailchimp/auth/index.js
@@ -0,0 +1,46 @@
+import generateAuthUrl from './generate-auth-url.js';
+import verifyCredentials from './verify-credentials.js';
+import isStillVerified from './is-still-verified.js';
+
+export default {
+ fields: [
+ {
+ key: 'oAuthRedirectUrl',
+ label: 'OAuth Redirect URL',
+ type: 'string',
+ required: true,
+ readOnly: true,
+ value: '{WEB_APP_URL}/app/mailchimp/connections/add',
+ placeholder: null,
+ description:
+ 'When asked to input a redirect URL in Mailchimp, enter the URL above.',
+ clickToCopy: true,
+ },
+ {
+ key: 'clientId',
+ label: 'Client ID',
+ type: 'string',
+ required: true,
+ readOnly: false,
+ value: null,
+ placeholder: null,
+ description: null,
+ clickToCopy: false,
+ },
+ {
+ key: 'clientSecret',
+ label: 'Client Secret',
+ type: 'string',
+ required: true,
+ readOnly: false,
+ value: null,
+ placeholder: null,
+ description: null,
+ clickToCopy: false,
+ },
+ ],
+
+ generateAuthUrl,
+ verifyCredentials,
+ isStillVerified,
+};
diff --git a/packages/backend/src/apps/mailchimp/auth/is-still-verified.js b/packages/backend/src/apps/mailchimp/auth/is-still-verified.js
new file mode 100644
index 00000000..b750ce14
--- /dev/null
+++ b/packages/backend/src/apps/mailchimp/auth/is-still-verified.js
@@ -0,0 +1,8 @@
+import getCurrentUser from '../common/get-current-user.js';
+
+const isStillVerified = async ($) => {
+ const currentUser = await getCurrentUser($);
+ return !!currentUser.user_id;
+};
+
+export default isStillVerified;
diff --git a/packages/backend/src/apps/mailchimp/auth/verify-credentials.js b/packages/backend/src/apps/mailchimp/auth/verify-credentials.js
new file mode 100644
index 00000000..efe7a049
--- /dev/null
+++ b/packages/backend/src/apps/mailchimp/auth/verify-credentials.js
@@ -0,0 +1,40 @@
+import getCurrentUser from '../common/get-current-user.js';
+
+const verifyCredentials = async ($) => {
+ const oauthRedirectUrlField = $.app.auth.fields.find(
+ (field) => field.key == 'oAuthRedirectUrl'
+ );
+ const redirectUri = oauthRedirectUrlField.value;
+ const params = new URLSearchParams({
+ grant_type: 'authorization_code',
+ client_id: $.auth.data.clientId,
+ client_secret: $.auth.data.clientSecret,
+ redirect_uri: redirectUri,
+ code: $.auth.data.code,
+ });
+
+ const { data } = await $.http.post(
+ 'https://login.mailchimp.com/oauth2/token',
+ params.toString()
+ );
+
+ await $.auth.set({
+ accessToken: data.access_token,
+ tokenType: data.token_type,
+ });
+
+ const currentUser = await getCurrentUser($);
+
+ await $.auth.set({
+ clientId: $.auth.data.clientId,
+ clientSecret: $.auth.data.clientSecret,
+ scope: $.auth.data.scope,
+ idToken: data.id_token,
+ expiresIn: data.expires_in,
+ refreshToken: data.refresh_token,
+ serverPrefix: currentUser.dc,
+ screenName: currentUser.login.login_name,
+ });
+};
+
+export default verifyCredentials;
diff --git a/packages/backend/src/apps/mailchimp/common/add-auth-header.js b/packages/backend/src/apps/mailchimp/common/add-auth-header.js
new file mode 100644
index 00000000..141e1bdc
--- /dev/null
+++ b/packages/backend/src/apps/mailchimp/common/add-auth-header.js
@@ -0,0 +1,12 @@
+const addAuthHeader = ($, requestConfig) => {
+ if (
+ !requestConfig.additionalProperties?.skipAddingAuthHeader &&
+ $.auth.data?.accessToken
+ ) {
+ requestConfig.headers.Authorization = `Bearer ${$.auth.data.accessToken}`;
+ }
+
+ return requestConfig;
+};
+
+export default addAuthHeader;
diff --git a/packages/backend/src/apps/mailchimp/common/get-current-user.js b/packages/backend/src/apps/mailchimp/common/get-current-user.js
new file mode 100644
index 00000000..1c27f1a3
--- /dev/null
+++ b/packages/backend/src/apps/mailchimp/common/get-current-user.js
@@ -0,0 +1,18 @@
+const getCurrentUser = async ($) => {
+ const { data: currentUser } = await $.http.get(
+ 'https://login.mailchimp.com/oauth2/metadata',
+ {
+ headers: {
+ Authorization: `OAuth ${$.auth.data.accessToken}`,
+ },
+ additionalProperties: {
+ skipAddingAuthHeader: true,
+ skipAddingBaseUrl: true,
+ },
+ }
+ );
+
+ return currentUser;
+};
+
+export default getCurrentUser;
diff --git a/packages/backend/src/apps/mailchimp/common/set-base-url.js b/packages/backend/src/apps/mailchimp/common/set-base-url.js
new file mode 100644
index 00000000..6ace2347
--- /dev/null
+++ b/packages/backend/src/apps/mailchimp/common/set-base-url.js
@@ -0,0 +1,10 @@
+const setBaseUrl = ($, requestConfig) => {
+ const serverPrefix = $.auth.data.serverPrefix;
+ if (!requestConfig.additionalProperties?.skipAddingBaseUrl && serverPrefix) {
+ requestConfig.baseURL = `https://${serverPrefix}.api.mailchimp.com`;
+ }
+
+ return requestConfig;
+};
+
+export default setBaseUrl;
diff --git a/packages/backend/src/apps/mailchimp/dynamic-data/index.js b/packages/backend/src/apps/mailchimp/dynamic-data/index.js
new file mode 100644
index 00000000..1e7045fa
--- /dev/null
+++ b/packages/backend/src/apps/mailchimp/dynamic-data/index.js
@@ -0,0 +1,6 @@
+import listAudiences from './list-audiences/index.js';
+import listCampaigns from './list-campaigns/index.js';
+import listTags from './list-segments-or-tags/index.js';
+import listTemplates from './list-templates/index.js';
+
+export default [listAudiences, listCampaigns, listTags, listTemplates];
diff --git a/packages/backend/src/apps/mailchimp/dynamic-data/list-audiences/index.js b/packages/backend/src/apps/mailchimp/dynamic-data/list-audiences/index.js
new file mode 100644
index 00000000..05646057
--- /dev/null
+++ b/packages/backend/src/apps/mailchimp/dynamic-data/list-audiences/index.js
@@ -0,0 +1,40 @@
+export default {
+ name: 'List audiences',
+ key: 'listAudiences',
+
+ async run($) {
+ const audiences = {
+ data: [],
+ };
+ let hasMore = false;
+
+ const params = {
+ sort_field: 'date_created',
+ sort_dir: 'DESC',
+ count: 1000,
+ offset: 0,
+ };
+
+ do {
+ const { data } = await $.http.get('/3.0/lists', { params });
+ params.offset = params.offset + params.count;
+
+ if (data?.lists) {
+ for (const audience of data.lists) {
+ audiences.data.push({
+ value: audience.id,
+ name: audience.name,
+ });
+ }
+ }
+
+ if (data.total_items > params.offset) {
+ hasMore = true;
+ } else {
+ hasMore = false;
+ }
+ } while (hasMore);
+
+ return audiences;
+ },
+};
diff --git a/packages/backend/src/apps/mailchimp/dynamic-data/list-campaigns/index.js b/packages/backend/src/apps/mailchimp/dynamic-data/list-campaigns/index.js
new file mode 100644
index 00000000..eef77523
--- /dev/null
+++ b/packages/backend/src/apps/mailchimp/dynamic-data/list-campaigns/index.js
@@ -0,0 +1,42 @@
+export default {
+ name: 'List campaigns',
+ key: 'listCampaigns',
+
+ async run($) {
+ const campaigns = {
+ data: [],
+ };
+ let hasMore = false;
+ const audienceId = $.step.parameters.audienceId;
+
+ const params = {
+ list_id: audienceId,
+ sort_field: 'create_time',
+ sort_dir: 'DESC',
+ count: 1000,
+ offset: 0,
+ };
+
+ do {
+ const { data } = await $.http.get('/3.0/campaigns', { params });
+ params.offset = params.offset + params.count;
+
+ if (data?.campaigns) {
+ for (const campaign of data.campaigns) {
+ campaigns.data.push({
+ value: campaign.id,
+ name: campaign.settings.title || campaign.settings.subject_line || 'Unnamed campaign',
+ });
+ }
+ }
+
+ if (data.total_items > params.offset) {
+ hasMore = true;
+ } else {
+ hasMore = false;
+ }
+ } while (hasMore);
+
+ return campaigns;
+ },
+};
diff --git a/packages/backend/src/apps/mailchimp/dynamic-data/list-segments-or-tags/index.js b/packages/backend/src/apps/mailchimp/dynamic-data/list-segments-or-tags/index.js
new file mode 100644
index 00000000..282345cf
--- /dev/null
+++ b/packages/backend/src/apps/mailchimp/dynamic-data/list-segments-or-tags/index.js
@@ -0,0 +1,44 @@
+export default {
+ name: 'List segments or tags',
+ key: 'listSegmentsOrTags',
+
+ async run($) {
+ const segmentsOrTags = {
+ data: [],
+ };
+ const audienceId = $.step.parameters.audienceId;
+
+ if (!audienceId) {
+ return segmentsOrTags;
+ }
+
+ const {
+ data: { tags: allTags },
+ } = await $.http.get(`/3.0/lists/${audienceId}/tag-search`);
+
+ const {
+ data: { segments },
+ } = await $.http.get(`/3.0/lists/${audienceId}/segments`);
+
+ const mergedArray = [...allTags, ...segments].reduce(
+ (accumulator, current) => {
+ if (!accumulator.some((item) => item.id === current.id)) {
+ accumulator.push(current);
+ }
+ return accumulator;
+ },
+ []
+ );
+
+ if (mergedArray?.length) {
+ for (const tagOrSegment of mergedArray) {
+ segmentsOrTags.data.push({
+ value: tagOrSegment.id,
+ name: tagOrSegment.name,
+ });
+ }
+ }
+
+ return segmentsOrTags;
+ },
+};
diff --git a/packages/backend/src/apps/mailchimp/dynamic-data/list-templates/index.js b/packages/backend/src/apps/mailchimp/dynamic-data/list-templates/index.js
new file mode 100644
index 00000000..9f67f812
--- /dev/null
+++ b/packages/backend/src/apps/mailchimp/dynamic-data/list-templates/index.js
@@ -0,0 +1,30 @@
+export default {
+ name: 'List templates',
+ key: 'listTemplates',
+
+ async run($) {
+ const templates = {
+ data: [],
+ };
+
+ const params = {
+ sort_field: 'date_created',
+ sort_dir: 'DESC',
+ count: 1000,
+ offset: 0,
+ };
+
+ const { data } = await $.http.get('/3.0/templates', { params });
+
+ if (data?.templates) {
+ for (const template of data.templates) {
+ templates.data.push({
+ value: template.id,
+ name: template.name,
+ });
+ }
+ }
+
+ return templates;
+ },
+};
diff --git a/packages/backend/src/apps/mailchimp/index.js b/packages/backend/src/apps/mailchimp/index.js
new file mode 100644
index 00000000..c3b01e63
--- /dev/null
+++ b/packages/backend/src/apps/mailchimp/index.js
@@ -0,0 +1,23 @@
+import defineApp from '../../helpers/define-app.js';
+import addAuthHeader from './common/add-auth-header.js';
+import setBaseUrl from './common/set-base-url.js';
+import auth from './auth/index.js';
+import triggers from './triggers/index.js';
+import dynamicData from './dynamic-data/index.js';
+import actions from './actions/index.js';
+
+export default defineApp({
+ name: 'Mailchimp',
+ key: 'mailchimp',
+ baseUrl: 'https://mailchimp.com',
+ apiBaseUrl: '',
+ iconUrl: '{BASE_URL}/apps/mailchimp/assets/favicon.svg',
+ authDocUrl: 'https://automatisch.io/docs/apps/mailchimp/connection',
+ primaryColor: '000000',
+ supportsConnections: true,
+ beforeRequest: [setBaseUrl, addAuthHeader],
+ auth,
+ triggers,
+ dynamicData,
+ actions,
+});
diff --git a/packages/backend/src/apps/mailchimp/triggers/email-opened/index.js b/packages/backend/src/apps/mailchimp/triggers/email-opened/index.js
new file mode 100644
index 00000000..16fc4a5c
--- /dev/null
+++ b/packages/backend/src/apps/mailchimp/triggers/email-opened/index.js
@@ -0,0 +1,101 @@
+import defineTrigger from '../../../../helpers/define-trigger.js';
+
+export default defineTrigger({
+ name: 'Email opened',
+ key: 'emailOpened',
+ pollInterval: 15,
+ description:
+ 'Triggers when a recipient opens an email as part of a particular campaign.',
+ arguments: [
+ {
+ label: 'Audience',
+ key: 'audienceId',
+ type: 'dropdown',
+ required: true,
+ description: '',
+ variables: true,
+ source: {
+ type: 'query',
+ name: 'getDynamicData',
+ arguments: [
+ {
+ name: 'key',
+ value: 'listAudiences',
+ },
+ ],
+ },
+ },
+ {
+ label: 'Campaign Type',
+ key: 'campaignType',
+ type: 'dropdown',
+ required: true,
+ description: '',
+ variables: true,
+ options: [
+ {
+ label: 'Campaign',
+ value: 'campaign',
+ },
+ ],
+ },
+ {
+ label: 'Campaign',
+ key: 'campaignId',
+ type: 'dropdown',
+ required: true,
+ dependsOn: ['parameters.audienceId'],
+ description: '',
+ variables: true,
+ source: {
+ type: 'query',
+ name: 'getDynamicData',
+ arguments: [
+ {
+ name: 'key',
+ value: 'listCampaigns',
+ },
+ {
+ name: 'parameters.audienceId',
+ value: '{parameters.audienceId}',
+ },
+ ],
+ },
+ },
+ ],
+
+ async run($) {
+ const campaignId = $.step.parameters.campaignId;
+ let hasMore = false;
+
+ const params = {
+ count: 1000,
+ offset: 0,
+ };
+
+ do {
+ const { data } = await $.http.get(
+ `/3.0/reports/${campaignId}/open-details`,
+ { params }
+ );
+ params.offset = params.offset + params.count;
+
+ if (data.members?.length) {
+ for (const member of data.members) {
+ $.pushTriggerItem({
+ raw: member,
+ meta: {
+ internalId: member.email_id,
+ },
+ });
+ }
+ }
+
+ if (data.total_items > params.offset) {
+ hasMore = true;
+ } else {
+ hasMore = false;
+ }
+ } while (hasMore);
+ },
+});
diff --git a/packages/backend/src/apps/mailchimp/triggers/index.js b/packages/backend/src/apps/mailchimp/triggers/index.js
new file mode 100644
index 00000000..d659e315
--- /dev/null
+++ b/packages/backend/src/apps/mailchimp/triggers/index.js
@@ -0,0 +1,5 @@
+import emailOpened from './email-opened/index.js';
+import newSubscribers from './new-subscribers/index.js';
+import newUnsubscribers from './new-unsubscribers/index.js';
+
+export default [emailOpened, newSubscribers, newUnsubscribers];
diff --git a/packages/backend/src/apps/mailchimp/triggers/new-subscribers/index.js b/packages/backend/src/apps/mailchimp/triggers/new-subscribers/index.js
new file mode 100644
index 00000000..6fe8e53f
--- /dev/null
+++ b/packages/backend/src/apps/mailchimp/triggers/new-subscribers/index.js
@@ -0,0 +1,105 @@
+import Crypto from 'crypto';
+import defineTrigger from '../../../../helpers/define-trigger.js';
+
+export default defineTrigger({
+ name: 'New subscribers',
+ key: 'newSubscribers',
+ type: 'webhook',
+ description: 'Triggers when a new subscriber is appended to an audience.',
+ arguments: [
+ {
+ label: 'Audience',
+ key: 'audienceId',
+ type: 'dropdown',
+ required: true,
+ description: '',
+ variables: true,
+ source: {
+ type: 'query',
+ name: 'getDynamicData',
+ arguments: [
+ {
+ name: 'key',
+ value: 'listAudiences',
+ },
+ ],
+ },
+ },
+ ],
+
+ async run($) {
+ const dataItem = {
+ raw: $.request.body,
+ meta: {
+ internalId: Crypto.randomUUID(),
+ },
+ };
+
+ $.pushTriggerItem(dataItem);
+ },
+
+ async testRun($) {
+ const audienceId = $.step.parameters.audienceId;
+
+ const computedWebhookEvent = {
+ data: {
+ id: Crypto.randomUUID(),
+ email: 'user@automatisch.io',
+ ip_opt: '127.0.0.1',
+ merges: {
+ EMAIL: 'user@automatisch.io',
+ FNAME: 'FNAME',
+ LNAME: 'LNAME',
+ PHONE: '',
+ ADDRESS: '',
+ BIRTHDAY: '',
+ },
+ web_id: Crypto.randomUUID(),
+ list_id: audienceId,
+ email_type: 'html',
+ },
+ type: 'subscribe',
+ fired_at: new Date().toLocaleString(),
+ };
+
+ const dataItem = {
+ raw: computedWebhookEvent,
+ meta: {
+ internalId: '',
+ },
+ };
+
+ $.pushTriggerItem(dataItem);
+ },
+
+ async registerHook($) {
+ const audienceId = $.step.parameters.audienceId;
+
+ const payload = {
+ url: $.webhookUrl,
+ events: {
+ subscribe: true,
+ },
+ sources: {
+ user: true,
+ admin: true,
+ api: true,
+ },
+ };
+
+ const response = await $.http.post(
+ `/3.0/lists/${audienceId}/webhooks`,
+ payload
+ );
+
+ await $.flow.setRemoteWebhookId(response.data.id);
+ },
+
+ async unregisterHook($) {
+ const audienceId = $.step.parameters.audienceId;
+
+ await $.http.delete(
+ `/3.0/lists/${audienceId}/webhooks/${$.flow.remoteWebhookId}`
+ );
+ },
+});
diff --git a/packages/backend/src/apps/mailchimp/triggers/new-unsubscribers/index.js b/packages/backend/src/apps/mailchimp/triggers/new-unsubscribers/index.js
new file mode 100644
index 00000000..f906efac
--- /dev/null
+++ b/packages/backend/src/apps/mailchimp/triggers/new-unsubscribers/index.js
@@ -0,0 +1,108 @@
+import Crypto from 'crypto';
+import defineTrigger from '../../../../helpers/define-trigger.js';
+
+export default defineTrigger({
+ name: 'New unsubscribers',
+ key: 'newUnsubscribers',
+ type: 'webhook',
+ description: 'Triggers when any existing subscriber opts out of an audience.',
+ arguments: [
+ {
+ label: 'Audience',
+ key: 'audienceId',
+ type: 'dropdown',
+ required: true,
+ description: '',
+ variables: true,
+ source: {
+ type: 'query',
+ name: 'getDynamicData',
+ arguments: [
+ {
+ name: 'key',
+ value: 'listAudiences',
+ },
+ ],
+ },
+ },
+ ],
+
+ async run($) {
+ const dataItem = {
+ raw: $.request.body,
+ meta: {
+ internalId: Crypto.randomUUID(),
+ },
+ };
+
+ $.pushTriggerItem(dataItem);
+ },
+
+ async testRun($) {
+ const audienceId = $.step.parameters.audienceId;
+
+ const computedWebhookEvent = {
+ data: {
+ id: Crypto.randomUUID(),
+ email: 'user@automatisch.io',
+ action: 'unsub',
+ ip_opt: '127.0.0.1',
+ merges: {
+ EMAIL: 'user@automatisch.io',
+ FNAME: 'FNAME',
+ LNAME: 'LNAME',
+ PHONE: '',
+ ADDRESS: '',
+ BIRTHDAY: '',
+ },
+ reason: 'manual',
+ web_id: Crypto.randomUUID(),
+ list_id: audienceId,
+ email_type: 'html',
+ campaign_id: Crypto.randomUUID(),
+ },
+ type: 'unsubscribe',
+ fired_at: new Date().toLocaleString(),
+ };
+
+ const dataItem = {
+ raw: computedWebhookEvent,
+ meta: {
+ internalId: '',
+ },
+ };
+
+ $.pushTriggerItem(dataItem);
+ },
+
+ async registerHook($) {
+ const audienceId = $.step.parameters.audienceId;
+
+ const payload = {
+ url: $.webhookUrl,
+ events: {
+ unsubscribe: true,
+ },
+ sources: {
+ user: true,
+ admin: true,
+ api: true,
+ },
+ };
+
+ const response = await $.http.post(
+ `/3.0/lists/${audienceId}/webhooks`,
+ payload
+ );
+
+ await $.flow.setRemoteWebhookId(response.data.id);
+ },
+
+ async unregisterHook($) {
+ const audienceId = $.step.parameters.audienceId;
+
+ await $.http.delete(
+ `/3.0/lists/${audienceId}/webhooks/${$.flow.remoteWebhookId}`
+ );
+ },
+});
diff --git a/packages/docs/pages/.vitepress/config.js b/packages/docs/pages/.vitepress/config.js
index 7e4785ba..0912d90b 100644
--- a/packages/docs/pages/.vitepress/config.js
+++ b/packages/docs/pages/.vitepress/config.js
@@ -252,6 +252,12 @@ export default defineConfig({
{ text: 'Connection', link: '/apps/invoice-ninja/connection' },
],
},
+ {
+ text: 'Mailchimp',
+ collapsible: true,
+ collapsed: true,
+ items: [{ text: 'Connection', link: '/apps/mailchimp/connection' }],
+ },
{
text: 'MailerLite',
collapsible: true,
diff --git a/packages/docs/pages/apps/mailchimp/actions.md b/packages/docs/pages/apps/mailchimp/actions.md
new file mode 100644
index 00000000..4f527d3a
--- /dev/null
+++ b/packages/docs/pages/apps/mailchimp/actions.md
@@ -0,0 +1,14 @@
+---
+favicon: /favicons/mailchimp.svg
+items:
+ - name: Create campaign
+ desc: Creates a new campaign draft.
+ - name: Send campaign
+ desc: Sends a campaign draft.
+---
+
+
+
+
diff --git a/packages/docs/pages/apps/mailchimp/connection.md b/packages/docs/pages/apps/mailchimp/connection.md
new file mode 100644
index 00000000..41886994
--- /dev/null
+++ b/packages/docs/pages/apps/mailchimp/connection.md
@@ -0,0 +1,17 @@
+# Mailchimp
+
+:::info
+This page explains the steps you need to follow to set up the Mailchimp
+connection in Automatisch. If any of the steps are outdated, please let us know!
+:::
+
+1. Login to your [Mailchimp account](https://mailchimp.com/) to create an app.
+2. Click on the account image and go to your **profile** page.
+3. Click on the **Extras** tab and choose the **Registered apps**.
+4. Click on the **Register An App** button.
+5. Fill the registration form.
+6. Copy **OAuth Redirect URL** from Automatisch to **Redirect URI** field, and click on the **Create** button.
+7. Copy the **Your Client ID** value to the `Client ID` field on Automatisch.
+8. Copy the **Your Client Secret** value to the `Client Secret` field on Automatisch.
+9. Click **Submit** button on Automatisch.
+10. Congrats! Start using your new Mailchimp connection within the flows.
diff --git a/packages/docs/pages/apps/mailchimp/triggers.md b/packages/docs/pages/apps/mailchimp/triggers.md
new file mode 100644
index 00000000..cf303bfe
--- /dev/null
+++ b/packages/docs/pages/apps/mailchimp/triggers.md
@@ -0,0 +1,16 @@
+---
+favicon: /favicons/mailchimp.svg
+items:
+ - name: Email opened
+ desc: Triggers when a recipient opens an email as part of a particular campaign.
+ - name: New subscribers
+ desc: Triggers when a new subscriber is appended to an audience.
+ - name: New unsubscribers
+ desc: Triggers when any existing subscriber opts out of an audience.
+---
+
+
+
+
diff --git a/packages/docs/pages/guide/available-apps.md b/packages/docs/pages/guide/available-apps.md
index 60be7318..e2ace1b9 100644
--- a/packages/docs/pages/guide/available-apps.md
+++ b/packages/docs/pages/guide/available-apps.md
@@ -25,6 +25,7 @@ The following integrations are currently supported by Automatisch.
- [HTTP Request](/apps/http-request/actions)
- [HubSpot](/apps/hubspot/actions)
- [Invoice Ninja](/apps/invoice-ninja/triggers)
+- [Mailchimp](/apps/mailchimp/triggers)
- [MailerLite](/apps/mailerlite/triggers)
- [Mattermost](/apps/mattermost/actions)
- [Miro](/apps/miro/actions)
diff --git a/packages/docs/pages/public/favicons/mailchimp.svg b/packages/docs/pages/public/favicons/mailchimp.svg
new file mode 100644
index 00000000..55af85db
--- /dev/null
+++ b/packages/docs/pages/public/favicons/mailchimp.svg
@@ -0,0 +1 @@
+
\ No newline at end of file