backend-manager 5.0.148 → 5.0.149

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/CLAUDE.md +26 -0
  3. package/package.json +1 -1
  4. package/src/cli/commands/emulator.js +14 -4
  5. package/src/cli/commands/test.js +4 -10
  6. package/src/manager/cron/daily/ghostii-auto-publisher.js +25 -25
  7. package/src/manager/cron/frequent/abandoned-carts.js +7 -5
  8. package/src/manager/cron/frequent/email-queue.js +56 -0
  9. package/src/manager/events/auth/before-signin.js +3 -0
  10. package/src/manager/events/auth/on-delete.js +8 -0
  11. package/src/manager/events/firestore/payments-disputes/on-write.js +2 -1
  12. package/src/manager/events/firestore/payments-webhooks/on-write.js +9 -0
  13. package/src/manager/events/firestore/payments-webhooks/transitions/send-email.js +7 -21
  14. package/src/manager/functions/core/actions/api/admin/get-stats.js +2 -2
  15. package/src/manager/functions/core/actions/api/admin/send-email.js +14 -14
  16. package/src/manager/functions/core/actions/api/general/add-marketing-contact.js +22 -318
  17. package/src/manager/functions/core/actions/api/general/emails/general:download-app-link.js +1 -1
  18. package/src/manager/functions/core/actions/api/general/remove-marketing-contact.js +2 -185
  19. package/src/manager/functions/core/actions/api/general/send-email.js +1 -1
  20. package/src/manager/functions/core/actions/api/special/setup-electron-manager-client.js +2 -2
  21. package/src/manager/functions/core/actions/api/test/health.js +1 -0
  22. package/src/manager/helpers/api-manager.js +2 -2
  23. package/src/manager/helpers/user.js +3 -1
  24. package/src/manager/index.js +15 -10
  25. package/src/manager/libraries/email/constants.js +243 -0
  26. package/src/manager/libraries/email/index.js +145 -0
  27. package/src/manager/libraries/email/marketing/index.js +377 -0
  28. package/src/manager/libraries/email/providers/beehiiv.js +258 -0
  29. package/src/manager/libraries/email/providers/sendgrid.js +429 -0
  30. package/src/manager/libraries/{email.js → email/transactional/index.js} +91 -99
  31. package/src/manager/libraries/email/validation.js +168 -0
  32. package/src/manager/routes/admin/cron/post.js +3 -3
  33. package/src/manager/routes/admin/email/post.js +1 -1
  34. package/src/manager/routes/admin/stats/get.js +2 -2
  35. package/src/manager/routes/{app → brand}/get.js +1 -1
  36. package/src/manager/routes/general/email/templates/download-app-link.js +1 -1
  37. package/src/manager/routes/marketing/contact/delete.js +2 -164
  38. package/src/manager/routes/marketing/contact/post.js +45 -298
  39. package/src/manager/routes/marketing/contact/put.js +39 -0
  40. package/src/manager/routes/payments/cancel/post.js +11 -0
  41. package/src/manager/routes/special/electron-client/post.js +3 -3
  42. package/src/manager/routes/test/health/get.js +1 -0
  43. package/src/manager/routes/user/data-request/delete.js +2 -2
  44. package/src/manager/routes/user/data-request/get.js +2 -2
  45. package/src/manager/routes/user/data-request/post.js +2 -2
  46. package/src/manager/routes/user/delete.js +1 -1
  47. package/src/manager/routes/user/feedback/post.js +12 -8
  48. package/src/manager/routes/user/signup/post.js +48 -37
  49. package/src/manager/schemas/admin/email/post.js +4 -4
  50. package/src/manager/schemas/marketing/contact/delete.js +3 -1
  51. package/src/manager/schemas/marketing/contact/post.js +3 -1
  52. package/src/manager/schemas/marketing/contact/put.js +6 -0
  53. package/src/manager/schemas/special/electron-client/post.js +2 -2
  54. package/src/manager/schemas/user/feedback/post.js +2 -2
  55. package/src/test/run-tests.js +1 -1
  56. package/src/test/runner.js +22 -10
  57. package/src/test/test-accounts.js +9 -0
  58. package/src/test/utils/extended-mode-warning.js +11 -0
  59. package/test/events/payments/journey-payments-cancel-endpoint.js +11 -0
  60. package/test/events/payments/journey-payments-trial-cancel.js +11 -0
  61. package/test/functions/admin/edit-post.js +2 -2
  62. package/test/functions/admin/write-repo-content.js +2 -2
  63. package/test/functions/general/add-marketing-contact.js +21 -23
  64. package/test/helpers/email-validation.js +420 -0
  65. package/test/helpers/email.js +119 -6
  66. package/test/helpers/marketing-lifecycle.js +121 -0
  67. package/test/helpers/user.js +2 -2
  68. package/test/routes/admin/create-post.js +2 -2
  69. package/test/routes/admin/post.js +2 -2
  70. package/test/routes/admin/repo-content.js +2 -2
  71. package/test/routes/marketing/contact.js +21 -24
  72. package/test/routes/payments/cancel.js +18 -0
@@ -138,6 +138,11 @@ Manager.prototype.init = function (exporter, options) {
138
138
  (_objValue, srcValue) => isArray(srcValue) ? srcValue : undefined,
139
139
  );
140
140
 
141
+ // Expose config on the constructor for static access by internal libraries.
142
+ // Since Node.js caches require(), any `require('./index.js')` returns this same
143
+ // Manager function with .config already set — no need for setConfig() patterns.
144
+ Manager.config = self.config;
145
+
141
146
  // Set PAYPAL_CLIENT_ID from config (clientId is public, not a secret — lives in config, not .env)
142
147
  process.env.PAYPAL_CLIENT_ID = process.env.PAYPAL_CLIENT_ID || self.config?.payment?.processors?.paypal?.clientId || '';
143
148
 
@@ -145,12 +150,12 @@ Manager.prototype.init = function (exporter, options) {
145
150
  process.env.CHARGEBEE_SITE = process.env.CHARGEBEE_SITE || self.config?.payment?.processors?.chargebee?.site || '';
146
151
 
147
152
  // Resolve legacy paths
148
- // TODO: Remove this in future versions (after we migrate to removing app.id from config)
153
+ // TODO: Remove this in future versions (after all consumers migrate to brand.id)
149
154
  self.config.app = self.config.app || {};
150
- self.config.app.id = self.config.brand.id || self.config.app.id || null;
155
+ self.config.brand.id = self.config.brand.id || self.config.app.id || null;
151
156
 
152
- // Get app ID
153
- const appId = self.config?.app?.id;
157
+ // Get brand ID
158
+ const brandId = self.config?.brand?.id;
154
159
 
155
160
  // Set log
156
161
  if (options.logSavePath) {
@@ -267,13 +272,13 @@ Manager.prototype.init = function (exporter, options) {
267
272
  self.assistant.log('Resolved backendManagerConfigPath', self.project.backendManagerConfigPath);
268
273
  }
269
274
 
270
- if (!appId) {
271
- self.assistant.warn('⚠️ Missing config.app.id');
275
+ if (!brandId) {
276
+ self.assistant.warn('⚠️ Missing config.brand.id');
272
277
  }
273
278
 
274
279
  // Setup sentry
275
280
  if (self.options.sentry) {
276
- const sentryRelease = `${appId || self.project.projectId}@${self.package.version}`;
281
+ const sentryRelease = `${brandId || self.project.projectId}@${self.package.version}`;
277
282
  const sentryDSN = self.config?.sentry?.dsn || '';
278
283
  // self.assistant.log('Sentry', sentryRelease, sentryDSN);
279
284
 
@@ -310,8 +315,8 @@ Manager.prototype.init = function (exporter, options) {
310
315
  // self.app = self.libraries.initializedAdmin;
311
316
 
312
317
  const loadedProjectId = serviceAccount.project_id;
313
- if (!loadedProjectId || !loadedProjectId.includes(appId)) {
314
- self.assistant.error(`Loaded app may have wrong service account: ${loadedProjectId} =/= ${appId}`);
318
+ if (!loadedProjectId || !loadedProjectId.includes(brandId)) {
319
+ self.assistant.error(`Loaded app may have wrong service account: ${loadedProjectId} =/= ${brandId}`);
315
320
  }
316
321
  }
317
322
  }
@@ -534,7 +539,7 @@ Manager.prototype.Metadata = function () {
534
539
 
535
540
  Manager.prototype.Email = function (assistant) {
536
541
  const self = this;
537
- self.libraries.Email = self.libraries.Email || require('./libraries/email.js');
542
+ self.libraries.Email = self.libraries.Email || require('./libraries/email/index.js');
538
543
  return new self.libraries.Email(assistant);
539
544
  };
540
545
 
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Shared constants for email libraries (transactional + marketing)
3
+ *
4
+ * SSOT for templates, ASM groups, and semantic senders.
5
+ * Used by: transactional/index.js, marketing/index.js, providers/*
6
+ */
7
+
8
+ // Template shortcut map — callers use readable paths instead of SendGrid IDs
9
+ // Paths mirror the email website structure: {category}/{subcategory}/{name}
10
+ const TEMPLATES = {
11
+ // v2 templates
12
+ 'main/basic/card': 'd-1cd2eee44b6340268c964cd7971d49b9',
13
+ 'main/engagement/feedback': 'd-319ab5c9d5074b21926a93562d6f41f6',
14
+ 'main/misc/app-download-link': 'd-fc8b4834d7e1472896fe7e46152029f4',
15
+ 'main/order/confirmation': 'd-5371ac2b4e3b490bbce51bfc2922ece8',
16
+ 'main/order/payment-failed': 'd-e56af0ac62364bfb9e50af02854e2cd3',
17
+ 'main/order/payment-recovered': 'd-d6dbd17a260a4755b34a852ba09c2454',
18
+ 'main/order/cancellation-requested': 'd-78074f3e8c844146bf263b86fc8d5ecf',
19
+ 'main/order/cancelled': 'd-39041132e6b24e5ebf0e95bce2d94dba',
20
+ 'main/order/plan-changed': 'd-399086311bbb48b4b77bc90b20fb9d0a',
21
+ 'main/order/trial-ending': 'd-af8ab499cbfb4d56918b4118f44343b0',
22
+ 'main/order/refunded': 'd-aa47fdbffa2b4ca9b73b6256e963e49f',
23
+ 'main/order/abandoned-cart': 'd-d8b3fa67e2b44b398dc280d0576bf1b7',
24
+ };
25
+
26
+ // "default" resolves to the basic card template
27
+ TEMPLATES['default'] = TEMPLATES['main/basic/card'];
28
+
29
+ // Group shortcut map — SendGrid ASM group IDs
30
+ // Rename these in SendGrid dashboard to match the comments
31
+ const GROUPS = {
32
+ 'orders': 16223, // BEM - Order Updates
33
+ 'hello': 35092, // BEM - Onboarding
34
+ 'account': 25927, // BEM - Account
35
+ 'marketing': 25928, // BEM - Marketing & Promotions
36
+ 'newsletter': 28096, // BEM - Newsletter
37
+ 'security': 35093, // BEM - Security
38
+ 'internal': 35094, // BEM - Internal Alerts
39
+ };
40
+
41
+ // Semantic sender categories — pass `sender: 'orders'` to auto-resolve from address, display name, and ASM group
42
+ const SENDERS = {
43
+ // Payment receipts, failed/recovered, cancellation, plan changes, refunds, trial ending
44
+ orders: {
45
+ localPart: 'orders',
46
+ displayName: '{brand} Orders',
47
+ group: GROUPS['orders'],
48
+ },
49
+ // Warm onboarding: welcome, 7-day checkup, feedback request
50
+ hello: {
51
+ localPart: 'hello',
52
+ displayName: '{brand}',
53
+ group: GROUPS['hello'],
54
+ },
55
+ // Transactional account actions: deletion, data requests
56
+ account: {
57
+ localPart: 'account',
58
+ displayName: '{brand} Account',
59
+ group: GROUPS['account'],
60
+ },
61
+ // Promotions, discounts, win-back, abandoned cart, app download link
62
+ marketing: {
63
+ localPart: 'offers',
64
+ displayName: '{brand}',
65
+ group: GROUPS['marketing'],
66
+ },
67
+ // Forgot password, 2FA, password reset
68
+ security: {
69
+ localPart: 'security',
70
+ displayName: '{brand} Security',
71
+ group: GROUPS['security'],
72
+ },
73
+ // Monthly newsletters, feature announcements, industry news
74
+ newsletter: {
75
+ localPart: 'newsletter',
76
+ displayName: '{brand}',
77
+ group: GROUPS['newsletter'],
78
+ },
79
+ // Dispute alerts, system notifications sent to brand contact
80
+ internal: {
81
+ localPart: 'alerts',
82
+ displayName: '{brand} Alerts',
83
+ group: GROUPS['internal'],
84
+ },
85
+ };
86
+
87
+ // Default marketing providers — SSOT for all provider loops
88
+ const DEFAULT_PROVIDERS = ['sendgrid', 'beehiiv'];
89
+
90
+ // SendGrid limit for scheduled emails (72 hours, but use 71 for buffer)
91
+ const SEND_AT_LIMIT = 71;
92
+
93
+ /**
94
+ * Convert SVG image URLs to PNG equivalents — email clients don't render SVGs.
95
+ * CDN naming convention: `-x.svg` -> `-1024.png`
96
+ */
97
+ function sanitizeImagesForEmail(images) {
98
+ const result = {};
99
+
100
+ for (const [key, value] of Object.entries(images)) {
101
+ if (typeof value === 'string' && value.endsWith('.svg')) {
102
+ result[key] = value.replace(/-x\.svg$/, '-1024.png');
103
+ } else {
104
+ result[key] = value;
105
+ }
106
+ }
107
+
108
+ return result;
109
+ }
110
+
111
+ /**
112
+ * URL-encode a value as base64
113
+ */
114
+ function encode(s) {
115
+ return encodeURIComponent(Buffer.from(String(s)).toString('base64'));
116
+ }
117
+
118
+ /**
119
+ * Create an Error with a code property for distinguishing build (400) vs send (500) failures.
120
+ */
121
+ function errorWithCode(message, code) {
122
+ const err = new Error(message);
123
+ err.code = code;
124
+ return err;
125
+ }
126
+
127
+ // Master field dictionary — the key IS the field name used in both providers.
128
+ //
129
+ // SendGrid: key is matched against custom field names at runtime (fetched + cached).
130
+ // Beehiiv: key is used directly as the custom field name.
131
+ //
132
+ // Source types:
133
+ // 'user' — read from user doc via _.get(userDoc, path)
134
+ // 'resolved' — read from User.resolveSubscription() output via path
135
+ // 'config' — read from Manager.config via _.get(config, path)
136
+ //
137
+ // To add a new tracked marketing field:
138
+ // 1. Add an entry here — the key becomes the field name in both providers
139
+ // 2. Add matching entry in OMEGA's src/lib/bem-fields.js (name, display, type)
140
+ // 3. Run OMEGA: npm start -- --service=sendgrid,beehiiv --brand=X
141
+ // 4. BEM resolves field IDs at runtime — no provider code changes needed
142
+ // 5. If 'resolved' source, ensure resolveFieldValues() computes it
143
+ const FIELDS = {
144
+ // Brand
145
+ brand_id: { source: 'config', path: 'brand.id', type: 'text' },
146
+
147
+ // User identity
148
+ user_auth_uid: { source: 'user', path: 'auth.uid', type: 'text' },
149
+ user_personal_country: { source: 'user', path: 'personal.location.country', type: 'text' },
150
+ user_metadata_signup_date: { source: 'user', path: 'metadata.created.timestamp', type: 'date' },
151
+ user_metadata_last_activity: { source: 'user', path: 'metadata.updated.timestamp', type: 'date' },
152
+
153
+ // Subscription
154
+ user_subscription_plan: { source: 'resolved', path: 'plan', type: 'text' },
155
+ user_subscription_status: { source: 'resolved', path: 'status', type: 'text' },
156
+ user_subscription_trialing: { source: 'resolved', path: 'trialing', type: 'text' },
157
+ user_subscription_cancelling: { source: 'resolved', path: 'cancelling', type: 'text' },
158
+ user_subscription_ever_paid: { source: 'resolved', path: 'everPaid', type: 'text' },
159
+ user_subscription_payment_processor: { source: 'user', path: 'subscription.payment.processor', type: 'text' },
160
+ user_subscription_payment_frequency: { source: 'user', path: 'subscription.payment.frequency', type: 'text' },
161
+ user_subscription_payment_price: { source: 'user', path: 'subscription.payment.price', type: 'number' },
162
+ user_subscription_payment_last_date: { source: 'user', path: 'subscription.payment.updatedBy.date.timestamp', type: 'date' },
163
+
164
+ // Attribution
165
+ user_attribution_utm_source: { source: 'user', path: 'attribution.utm.tags.utm_source', type: 'text' },
166
+ };
167
+
168
+
169
+ /**
170
+ * Resolve all field values from a user doc + config.
171
+ * Returns a map of semantic field names → resolved values (type-coerced).
172
+ * Providers use this internally to build their native field format.
173
+ *
174
+ * @param {object} userDoc - User document from Firestore
175
+ * @param {object} config - Manager.config
176
+ * @returns {object} Map of semantic name → value (e.g., { plan: 'basic', status: 'active', ... })
177
+ */
178
+ const _ = require('lodash');
179
+ const User = require('../../helpers/user.js');
180
+
181
+ function resolveFieldValues(userDoc, config) {
182
+ const resolved = User.resolveSubscription(userDoc);
183
+ const subscription = userDoc.subscription || {};
184
+
185
+ // Computed values from resolveSubscription() + raw status
186
+ const resolvedValues = {
187
+ plan: resolved.plan,
188
+ status: subscription.status || 'active',
189
+ everPaid: String(resolved.everPaid),
190
+ trialing: String(resolved.trialing),
191
+ cancelling: String(resolved.cancelling),
192
+ };
193
+
194
+ const result = {};
195
+
196
+ for (const [name, fieldConfig] of Object.entries(FIELDS)) {
197
+ let value;
198
+
199
+ if (fieldConfig.source === 'config') {
200
+ value = _.get(config, fieldConfig.path);
201
+ } else if (fieldConfig.source === 'resolved') {
202
+ value = resolvedValues[fieldConfig.path];
203
+ } else {
204
+ value = _.get(userDoc, fieldConfig.path);
205
+ }
206
+
207
+ if (value == null) {
208
+ continue;
209
+ }
210
+
211
+ // Coerce booleans to strings for text fields
212
+ if (fieldConfig.type === 'text' && typeof value === 'boolean') {
213
+ value = String(value);
214
+ }
215
+
216
+ // Coerce to number for number fields
217
+ if (fieldConfig.type === 'number' && typeof value !== 'number') {
218
+ value = Number(value) || 0;
219
+ }
220
+
221
+ // Skip epoch default dates (1970-01-01)
222
+ if (fieldConfig.type === 'date' && (!value || value === '1970-01-01T00:00:00.000Z')) {
223
+ continue;
224
+ }
225
+
226
+ result[name] = value;
227
+ }
228
+
229
+ return result;
230
+ }
231
+
232
+ module.exports = {
233
+ TEMPLATES,
234
+ GROUPS,
235
+ SENDERS,
236
+ FIELDS,
237
+ DEFAULT_PROVIDERS,
238
+ SEND_AT_LIMIT,
239
+ sanitizeImagesForEmail,
240
+ encode,
241
+ errorWithCode,
242
+ resolveFieldValues,
243
+ };
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Unified email library — transactional + marketing
3
+ *
4
+ * Usage:
5
+ * const email = Manager.Email(assistant);
6
+ *
7
+ * // Transactional (default)
8
+ * await email.send({ to, subject, template, ... });
9
+ * await email.build({ ... });
10
+ *
11
+ * // Marketing campaign
12
+ * await email.send({ type: 'marketing', name, subject, segments, ... });
13
+ *
14
+ * // Add a new contact (newsletter subscribe)
15
+ * await email.add({ email, firstName, lastName, source });
16
+ *
17
+ * // Sync full user data to SendGrid/Beehiiv (all custom fields)
18
+ * await email.sync(userDoc);
19
+ *
20
+ * // Remove contact from all providers
21
+ * await email.remove('user@example.com');
22
+ *
23
+ * // Campaign management
24
+ * await email.cancelCampaign(id);
25
+ * await email.getCampaign(id);
26
+ * await email.listCampaigns({ status: 'scheduled' });
27
+ */
28
+ const Transactional = require('./transactional/index.js');
29
+ const Marketing = require('./marketing/index.js');
30
+
31
+ function Email(assistant) {
32
+ const self = this;
33
+
34
+ self.assistant = assistant;
35
+ self.Manager = assistant.Manager;
36
+
37
+ // Compose internal modules
38
+ self._transactional = new Transactional(assistant);
39
+ self._marketing = new Marketing(assistant);
40
+
41
+ return self;
42
+ }
43
+
44
+ /**
45
+ * Send an email.
46
+ *
47
+ * @param {object} settings
48
+ * @param {string} [settings.type] - 'transactional' (default) or 'marketing'
49
+ *
50
+ * Transactional settings: { to, subject, template, sender, sendAt, data, ... }
51
+ * Marketing settings: { name, subject, template, sender, segments, lists, sendAt, ... }
52
+ *
53
+ * @returns {object} Result from the appropriate sender
54
+ */
55
+ Email.prototype.send = function (settings) {
56
+ const self = this;
57
+ const type = settings.type || 'transactional';
58
+
59
+ if (type === 'marketing') {
60
+ return self._marketing.sendCampaign(settings);
61
+ }
62
+
63
+ return self._transactional.send(settings);
64
+ };
65
+
66
+ /**
67
+ * Build a transactional email without sending it.
68
+ *
69
+ * @param {object} settings - Same as send() transactional settings
70
+ * @returns {object} SendGrid-ready email object
71
+ */
72
+ Email.prototype.build = function (settings) {
73
+ return this._transactional.build(settings);
74
+ };
75
+
76
+ /**
77
+ * Sync a user's data to marketing providers (SendGrid + Beehiiv).
78
+ *
79
+ * @param {object} userDoc - Full user document from Firestore
80
+ * @param {object} [options] - { providers: array of provider names (default: DEFAULT_PROVIDERS) }
81
+ * @returns {{ sendgrid?: object, beehiiv?: object }}
82
+ */
83
+ /**
84
+ * Add a new contact to marketing providers (lightweight, no full user doc needed).
85
+ *
86
+ * @param {object} options - { email, firstName, lastName, source, customFields, providers }
87
+ * @returns {{ sendgrid?: object, beehiiv?: object }}
88
+ */
89
+ Email.prototype.add = function (options) {
90
+ return this._marketing.add(options);
91
+ };
92
+
93
+ /**
94
+ * Sync a user's full data to marketing providers (SendGrid + Beehiiv).
95
+ *
96
+ * @param {object} userDoc - Full user document from Firestore
97
+ * @param {object} [options] - { providers: array of provider names (default: DEFAULT_PROVIDERS) }
98
+ * @returns {{ sendgrid?: object, beehiiv?: object }}
99
+ */
100
+ Email.prototype.sync = function (userDoc, options) {
101
+ return this._marketing.sync(userDoc, options);
102
+ };
103
+
104
+ /**
105
+ * Remove a contact from all marketing providers.
106
+ *
107
+ * @param {string} email - Email address to remove
108
+ * @param {object} [options] - { providers: array of provider names (default: DEFAULT_PROVIDERS) }
109
+ * @returns {{ sendgrid?: object, beehiiv?: object }}
110
+ */
111
+ Email.prototype.remove = function (email, options) {
112
+ return this._marketing.remove(email, options);
113
+ };
114
+
115
+ /**
116
+ * Cancel a scheduled marketing campaign.
117
+ *
118
+ * @param {string} campaignId - Single Send ID
119
+ * @returns {{ success: boolean, error?: string }}
120
+ */
121
+ Email.prototype.cancelCampaign = function (campaignId) {
122
+ return this._marketing.cancelCampaign(campaignId);
123
+ };
124
+
125
+ /**
126
+ * Get a marketing campaign by ID.
127
+ *
128
+ * @param {string} campaignId - Single Send ID
129
+ * @returns {object|null}
130
+ */
131
+ Email.prototype.getCampaign = function (campaignId) {
132
+ return this._marketing.getCampaign(campaignId);
133
+ };
134
+
135
+ /**
136
+ * List marketing campaigns.
137
+ *
138
+ * @param {object} [options] - { status: 'draft' | 'scheduled' | 'triggered' }
139
+ * @returns {Array<object>}
140
+ */
141
+ Email.prototype.listCampaigns = function (options) {
142
+ return this._marketing.listCampaigns(options);
143
+ };
144
+
145
+ module.exports = Email;