diff --git a/components/elastic_email/.gitignore b/components/elastic_email/.gitignore deleted file mode 100644 index ec761ccab7595..0000000000000 --- a/components/elastic_email/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.js -*.mjs -dist \ No newline at end of file diff --git a/components/elastic_email/actions/add-contact/add-contact.mjs b/components/elastic_email/actions/add-contact/add-contact.mjs new file mode 100644 index 0000000000000..000c90829af5e --- /dev/null +++ b/components/elastic_email/actions/add-contact/add-contact.mjs @@ -0,0 +1,104 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { + CONSENT_TRACKING_OPTIONS, + STATUS_OPTIONS, +} from "../../common/constants.mjs"; +import { parseObject } from "../../common/utils.mjs"; +import app from "../../elastic_email.app.mjs"; + +export default { + key: "elastic_email-add-contact", + name: "Add Contact to Mailing List", + description: "Adds a new contact to a mailing list. [See the documentation](https://elasticemail.com/developers/api-documentation/rest-api#operation/contactsPost)", + version: "0.0.1", + type: "action", + props: { + app, + email: { + propDefinition: [ + app, + "email", + ], + }, + listNames: { + propDefinition: [ + app, + "listNames", + ], + optional: true, + }, + status: { + type: "string", + label: "Status", + description: "The initial status of the contact.", + options: STATUS_OPTIONS, + optional: true, + }, + firstName: { + type: "string", + label: "First Name", + description: "The contact's first name.", + optional: true, + }, + lastName: { + type: "string", + label: "Last Name", + description: "The contact's last name.", + optional: true, + }, + customFields: { + type: "object", + label: "Custom Fields", + description: "A key-value collection of custom contact fields which can be used in the system. Only already existing custom fields will be saved.", + optional: true, + }, + consentIP: { + type: "string", + label: "Consent IP", + description: "IP address of consent to send this contact(s) your email. If not provided your current public IP address is used for consent.", + optional: true, + }, + consentDate: { + type: "string", + label: "Consent Date", + description: "Date of consent to send this contact(s) your email. If not provided current date is used for consent.", + optional: true, + }, + consentTracking: { + type: "string", + label: "Consent Tracking", + description: "Tracking of consent to send this contact(s) your email. Defaults to \"Unknown\".", + options: CONSENT_TRACKING_OPTIONS, + optional: true, + }, + }, + async run({ $ }) { + const response = await this.app.addContact({ + $, + params: { + listnames: parseObject(this.listNames), + }, + data: [ + { + Email: this.email, + Status: this.status, + FirstName: this.firstName, + LastName: this.lastName, + CustomFields: parseObject(this.customFields), + Consent: { + ConsentIP: this.consentIP, + ConsentDate: this.consentDate, + ConsentTracking: this.consentTracking, + }, + }, + ], + }); + + if (("success" in response) && response.success === "false") { + throw new ConfigurationError(response.error); + } + + $.export("$summary", `Successfully added contact ${this.email} to the mailing list`); + return response; + }, +}; diff --git a/components/elastic_email/actions/send-email/send-email.mjs b/components/elastic_email/actions/send-email/send-email.mjs new file mode 100644 index 0000000000000..17889d3e3187a --- /dev/null +++ b/components/elastic_email/actions/send-email/send-email.mjs @@ -0,0 +1,135 @@ +import { + BODY_CONTENT_TYPE_OPTIONS, + ENCODING_OPTIONS, +} from "../../common/constants.mjs"; +import { parseObject } from "../../common/utils.mjs"; +import app from "../../elastic_email.app.mjs"; + +export default { + key: "elastic_email-send-email", + name: "Send Email", + description: "Sends an email to one or more recipients. [See the documentation](https://elasticemail.com/developers/api-documentation/rest-api#operation/emailsPost)", + version: "0.0.1", + type: "action", + props: { + app, + recipients: { + type: "string[]", + label: "Recipients", + description: "List of recipients", + }, + from: { + type: "string", + label: "From", + description: "Your e-mail with an optional name (e.g.: email@domain.com)", + }, + bodyContentType: { + type: "string", + label: "Body Content Type", + description: "Type of body part", + options: BODY_CONTENT_TYPE_OPTIONS, + optional: true, + }, + bodyContent: { + type: "string", + label: "Body Content", + description: "Actual content of the body part", + optional: true, + }, + merge: { + type: "object", + label: "Merge", + description: "A key-value collection of custom merge fields, shared between recipients. Should be used in e-mail body like so: {firstname}, {lastname} etc.", + optional: true, + }, + replyTo: { + type: "string", + label: "Reply To", + description: "To what address should the recipients reply to (e.g. email@domain.com)", + optional: true, + }, + subject: { + type: "string", + label: "Subject", + description: "Default subject of email.", + optional: true, + }, + templateName: { + propDefinition: [ + app, + "templateName", + ], + optional: true, + }, + timeOffset: { + type: "integer", + label: "Time Offset", + description: "By how long should an e-mail be delayed (in minutes). Maximum is 35 days.", + optional: true, + }, + poolName: { + type: "string", + label: "Pool Name", + description: "Name of your custom IP Pool to be used in the sending process", + optional: true, + }, + channelName: { + type: "string", + label: "Channel Name", + description: "Name of selected channel.", + optional: true, + }, + encoding: { + type: "string", + label: "Encoding", + description: "Encoding type for the email headers", + options: ENCODING_OPTIONS, + optional: true, + }, + trackOpens: { + type: "boolean", + label: "Track Opens", + description: "Should the opens be tracked? If no value has been provided, Account's default setting will be used.", + optional: true, + }, + trackClicks: { + type: "boolean", + label: "Track Clicks", + description: "Should the clicks be tracked? If no value has been provided, Account's default setting will be used.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.app.sendBulkEmails({ + $, + data: { + Recipients: parseObject(this.recipients)?.map((item) => ({ + Email: item, + })), + Content: { + From: this.from, + Body: [ + { + ContentType: this.bodyContentType, + Body: this.bodyContent, + }, + ], + Merge: parseObject(this.merge), + ReplyTo: this.replyTo, + Subject: this.subject, + TemplateName: this.templateName, + }, + Options: { + TimeOffset: this.timeOffset, + PoolName: this.poolName, + ChannelName: this.channelName, + Encoding: this.encoding, + TrackOpens: this.trackOpens, + TrackClicks: this.trackClicks, + }, + }, + }); + $.export("$summary", `Emails sent successfully to ${this.recipients.join(", ")}`); + return response; + }, +}; diff --git a/components/elastic_email/actions/unsubscribe-contact/unsubscribe-contact.mjs b/components/elastic_email/actions/unsubscribe-contact/unsubscribe-contact.mjs new file mode 100644 index 0000000000000..440d2bffbc16e --- /dev/null +++ b/components/elastic_email/actions/unsubscribe-contact/unsubscribe-contact.mjs @@ -0,0 +1,28 @@ +import { parseObject } from "../../common/utils.mjs"; +import app from "../../elastic_email.app.mjs"; + +export default { + key: "elastic_email-unsubscribe-contact", + name: "Unsubscribe Contact", + description: "Unsubscribes a contact from future emails. [See the documentation](https://elasticemail.com/developers/api-documentation/rest-api#operation/suppressionsUnsubscribesPost)", + version: "0.0.1", + type: "action", + props: { + app, + unsubscribeEmails: { + propDefinition: [ + app, + "unsubscribeEmails", + ], + }, + }, + async run({ $ }) { + const parsedEmails = parseObject(this.unsubscribeEmails); + const response = await this.app.unsubscribeContact({ + $, + data: parsedEmails, + }); + $.export("$summary", `Unsubscribed ${parsedEmails.length} contact(s) successfully`); + return response; + }, +}; diff --git a/components/elastic_email/app/elastic_email.app.ts b/components/elastic_email/app/elastic_email.app.ts deleted file mode 100644 index fc3a1ba51a60f..0000000000000 --- a/components/elastic_email/app/elastic_email.app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineApp } from "@pipedream/types"; - -export default defineApp({ - type: "app", - app: "elastic_email", - propDefinitions: {}, - methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); - }, - }, -}); \ No newline at end of file diff --git a/components/elastic_email/common/constants.mjs b/components/elastic_email/common/constants.mjs new file mode 100644 index 0000000000000..3f80d84ac44bb --- /dev/null +++ b/components/elastic_email/common/constants.mjs @@ -0,0 +1,36 @@ +export const LIMIT = 100; + +export const STATUS_OPTIONS = [ + "Transactional", + "Engaged", + "Active", + "Bounced", + "Unsubscribed", + "Abuse", + "Inactive", + "Stale", + "NotConfirmed", +]; + +export const ENCODING_OPTIONS = [ + "UserProvided", + "None", + "Raw7bit", + "Raw8bit", + "QuotedPrintable", + "Base64", + "Uue", +]; + +export const CONSENT_TRACKING_OPTIONS = [ + "Unknown", + "Allow", + "Deny", +]; + +export const BODY_CONTENT_TYPE_OPTIONS = [ + "HTML", + "PlainText", + "AMP", + "CSS", +]; diff --git a/components/elastic_email/common/utils.mjs b/components/elastic_email/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/elastic_email/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/elastic_email/elastic_email.app.mjs b/components/elastic_email/elastic_email.app.mjs new file mode 100644 index 0000000000000..f9e25b70be3ea --- /dev/null +++ b/components/elastic_email/elastic_email.app.mjs @@ -0,0 +1,142 @@ +import { axios } from "@pipedream/platform"; +import { LIMIT } from "./common/constants.mjs"; + +export default { + type: "app", + app: "elastic_email", + propDefinitions: { + email: { + type: "string", + label: "Email", + description: "The contact's email", + }, + listNames: { + type: "string[]", + label: "List Names", + description: "Names of the mailing lists", + optional: true, + async options({ page }) { + const data = await this.listLists({ + params: { + limit: LIMIT, + offset: LIMIT * page, + }, + }); + + return data.map(({ ListName }) => ListName); + }, + }, + templateName: { + type: "string", + label: "Template Name", + description: "The name of template.", + async options({ page }) { + const data = await this.listTemplates({ + params: { + limit: LIMIT, + offset: LIMIT * page, + scopeType: "Personal", + }, + }); + + return data.map(({ Name }) => Name); + }, + }, + unsubscribeEmails: { + type: "string[]", + label: "Email Addresses", + description: "A list of email addresses to unsubscribe", + }, + }, + methods: { + _baseUrl() { + return "https://api.elasticemail.com/v4"; + }, + _headers() { + return { + "X-ElasticEmail-ApiKey": this.$auth.api_key, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + loadEvents(opts = {}) { + return this._makeRequest({ + path: "/events", + ...opts, + }); + }, + listContacts(opts = {}) { + return this._makeRequest({ + path: "/contacts", + ...opts, + }); + }, + listLists(opts = {}) { + return this._makeRequest({ + path: "/lists", + ...opts, + }); + }, + listTemplates(opts = {}) { + return this._makeRequest({ + path: "/templates", + ...opts, + }); + }, + sendBulkEmails(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/emails", + ...opts, + }); + }, + addContact(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/contacts", + ...opts, + }); + }, + unsubscribeContact(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/suppressions/unsubscribes", + ...opts, + }); + }, + async *paginate({ + fn, params = {}, maxResults = null, ...opts + }) { + let hasMore = false; + let count = 0; + let page = 0; + + do { + params.limit = LIMIT; + params.offset = LIMIT * page; + const data = await fn({ + params, + ...opts, + }); + for (const d of data) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + page++; + hasMore = data.length; + + } while (hasMore); + }, + }, +}; diff --git a/components/elastic_email/package.json b/components/elastic_email/package.json index 6dc000e6c86bb..aef531c563c61 100644 --- a/components/elastic_email/package.json +++ b/components/elastic_email/package.json @@ -1,16 +1,18 @@ { "name": "@pipedream/elastic_email", - "version": "0.0.2", + "version": "0.1.0", "description": "Pipedream Elastic Email Components", - "main": "dist/app/elastic_email.app.mjs", + "main": "elastic_email.app.mjs", "keywords": [ "pipedream", "elastic_email" ], - "files": ["dist"], "homepage": "https://pipedream.com/apps/elastic_email", "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" } } diff --git a/components/elastic_email/sources/common/base.mjs b/components/elastic_email/sources/common/base.mjs new file mode 100644 index 0000000000000..2ade55a7bdb51 --- /dev/null +++ b/components/elastic_email/sources/common/base.mjs @@ -0,0 +1,73 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import app from "../../elastic_email.app.mjs"; + +export default { + props: { + app, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || "1970-01-01T01:01:01"; + }, + _setLastDate(lastDate) { + this.db.set("lastDate", lastDate); + }, + getFunction() { + return this.app.loadEvents; + }, + getEventType() { + return ""; + }, + async emitEvent(maxResults = false) { + const lastDate = this._getLastDate(); + const dateField = this.getDateField(); + const idField = this.getIdField(); + + const response = this.app.paginate({ + fn: this.getFunction(), + maxResults, + params: { + from: lastDate, + }, + }); + + let responseArray = []; + for await (const item of response) { + const eventType = this.getEventType(); + if (eventType && !eventType.includes(item.EventType)) continue; + if (Date.parse(item[dateField]) <= Date.parse(lastDate)) break; + responseArray.push(item); + } + + if (responseArray.length) { + if (maxResults && (responseArray.length > maxResults)) { + responseArray.length = maxResults; + } + this._setLastDate(responseArray[0][dateField]); + } + + for (const item of responseArray.reverse()) { + this.$emit(item, { + id: item[idField], + summary: this.getSummary(item), + ts: Date.parse(item[dateField] || new Date()), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/elastic_email/sources/new-contact-added/new-contact-added.mjs b/components/elastic_email/sources/new-contact-added/new-contact-added.mjs new file mode 100644 index 0000000000000..0037da2861785 --- /dev/null +++ b/components/elastic_email/sources/new-contact-added/new-contact-added.mjs @@ -0,0 +1,28 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "elastic_email-new-contact-added", + name: "New Contact Added", + description: "Emit new event when a new contact is added to a mailing list. [See the documentation](https://elasticemail.com/developers/api-documentation/rest-api#operation/contactsGet)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getFunction() { + return this.app.listContacts; + }, + getDateField() { + return "DateAdded"; + }, + getIdField() { + return "Email"; + }, + getSummary(item) { + return `New contact added: ${item.Email}`; + }, + }, + sampleEmit, +}; diff --git a/components/elastic_email/sources/new-contact-added/test-event.mjs b/components/elastic_email/sources/new-contact-added/test-event.mjs new file mode 100644 index 0000000000000..6dc16798fb2ca --- /dev/null +++ b/components/elastic_email/sources/new-contact-added/test-event.mjs @@ -0,0 +1,32 @@ +export default { + "Email": "mail@example.com", + "Status": "Transactional", + "FirstName": "Fred", + "LastName": "Flintstone", + "CustomFields": { + "city": "New York", + "age": "34" + }, + "Consent": { + "ConsentIP": "192.168.0.1", + "ConsentDate": "1/1/2015 0:00:00 AM", + "ConsentTracking": "Unknown" + }, + "Source": "DeliveryApi", + "DateAdded": "2001-01-01T12:00:00", + "DateUpdated": "2001-01-01T12:00:00", + "StatusChangeDate": "2001-01-01T12:00:00", + "Activity": { + "TotalSent": "1000", + "TotalOpened": "1000", + "TotalClicked": "1000", + "TotalFailed": "1000", + "LastSent": null, + "LastOpened": "2014-01-01", + "LastClicked": null, + "LastFailed": null, + "LastIP": "string", + "ErrorCode": null, + "FriendlyErrorMessage": "string" + } + } \ No newline at end of file diff --git a/components/elastic_email/sources/new-email-click/new-email-click.mjs b/components/elastic_email/sources/new-email-click/new-email-click.mjs new file mode 100644 index 0000000000000..d92d51ea2ef27 --- /dev/null +++ b/components/elastic_email/sources/new-email-click/new-email-click.mjs @@ -0,0 +1,30 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "elastic_email-new-email-click", + name: "New Email Click", + description: "Emit new event when a recipient clicks a link in an email. [See the documentation](https://elasticemail.com/developers/api-documentation/rest-api#operation/eventsGet).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventType() { + return [ + "Click", + ]; + }, + getDateField() { + return "EventDate"; + }, + getIdField() { + return "MsgID"; + }, + getSummary() { + return "New event click"; + }, + }, + sampleEmit, +}; diff --git a/components/elastic_email/sources/new-email-click/test-event.mjs b/components/elastic_email/sources/new-email-click/test-event.mjs new file mode 100644 index 0000000000000..817c00b109188 --- /dev/null +++ b/components/elastic_email/sources/new-email-click/test-event.mjs @@ -0,0 +1,15 @@ +export default { + "TransactionID": "TransactionID", + "MsgID": "ABCDE_9RPhSWiaJq_ab1g1", + "FromEmail": "sender@yourdomain.com", + "To": "2001-01-01T01:01:01", + "Subject": "Hello!", + "EventType": "Click", + "EventDate": "2019-08-24T14:15:22Z", + "ChannelName": "Channel01", + "MessageCategory": "Unknown", + "NextTryOn": "2001-01-01T12:00:00", + "Message": "Lorem ipsum", + "IPAddress": "string", + "PoolName": "string" + } \ No newline at end of file diff --git a/components/elastic_email/sources/new-email-open/new-email-open.mjs b/components/elastic_email/sources/new-email-open/new-email-open.mjs new file mode 100644 index 0000000000000..add9485056fae --- /dev/null +++ b/components/elastic_email/sources/new-email-open/new-email-open.mjs @@ -0,0 +1,30 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "elastic_email-new-email-open", + name: "New Email Open", + description: "Emit new event when a recipient opens an email. [See the documentation](https://elasticemail.com/developers/api-documentation/rest-api#operation/eventsGet).", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventType() { + return [ + "Open", + ]; + }, + getDateField() { + return "EventDate"; + }, + getIdField() { + return "MsgID"; + }, + getSummary() { + return "New Email opened"; + }, + }, + sampleEmit, +}; diff --git a/components/elastic_email/sources/new-email-open/test-event.mjs b/components/elastic_email/sources/new-email-open/test-event.mjs new file mode 100644 index 0000000000000..9e2c8df36cc34 --- /dev/null +++ b/components/elastic_email/sources/new-email-open/test-event.mjs @@ -0,0 +1,15 @@ +export default { + "TransactionID": "TransactionID", + "MsgID": "ABCDE_9RPhSWiaJq_ab1g1", + "FromEmail": "sender@yourdomain.com", + "To": "2001-01-01T01:01:01", + "Subject": "Hello!", + "EventType": "Open", + "EventDate": "2019-08-24T14:15:22Z", + "ChannelName": "Channel01", + "MessageCategory": "Unknown", + "NextTryOn": "2001-01-01T12:00:00", + "Message": "Lorem ipsum", + "IPAddress": "string", + "PoolName": "string" + } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d442d83ee7601..ddd15470b780a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3889,7 +3889,11 @@ importers: components/elastic_cloud: {} - components/elastic_email: {} + components/elastic_email: + dependencies: + '@pipedream/platform': + specifier: ^3.0.3 + version: 3.0.3 components/elasticemail: {}