payload-plugin-newsletter 0.20.6 → 0.21.0

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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,74 @@
1
+ ## [0.21.0] - 2025-01-31
2
+
3
+ ### Added
4
+ - **Webhook Support**: Real-time webhook integration for Broadcast provider
5
+ - Automatic updates for subscriber events (subscribed/unsubscribed)
6
+ - Real-time broadcast status updates (scheduled, in_progress, sent, etc.)
7
+ - HMAC-SHA256 signature verification for security
8
+ - Webhook configuration UI in Newsletter Settings
9
+ - Manual webhook registration with setup instructions
10
+
11
+ ### Changed
12
+ - **BREAKING**: Removed polling-based unsubscribe synchronization
13
+ - Webhooks now provide real-time updates instead of scheduled polling
14
+ - Removed `unsubscribeSync` configuration option
15
+ - Removed `afterUnsubscribeSync` hook
16
+ - Removed sync job infrastructure
17
+
18
+ ### Security
19
+ - Added webhook signature verification with timestamp validation
20
+ - Webhook secrets stored securely in Newsletter Settings
21
+
22
+ ### Migration Guide
23
+ If you were using unsubscribe sync:
24
+ 1. Remove any `unsubscribeSync` configuration from your plugin setup
25
+ 2. Configure webhooks in your Broadcast dashboard (see README)
26
+ 3. Add the webhook secret to your Newsletter Settings
27
+
28
+ ## [0.20.7] - 2025-08-02
29
+
30
+ ### Added
31
+ - **Comprehensive Email Wrapper System** - Unified email rendering that uses the same wrapper for previews and actual sends
32
+ - Preview component now passes ALL form fields to the preview endpoint as `documentData`
33
+ - Custom wrappers receive complete document data, not just content
34
+ - Consistent HTML output between preview and actual email sends
35
+ - Support for accessing any custom fields added to broadcasts collection
36
+ - Wrapper is applied during actual send operations, not just previews
37
+
38
+ ### Enhanced
39
+ - **Email Preview Component** - Builds document data from all form fields for wrapper access
40
+ - Collects all field values using `useFormFields` hook
41
+ - Passes complete form state to preview endpoint
42
+ - Enables custom wrappers to access metadata like slug, issueNumber, custom fields
43
+
44
+ - **Preview Endpoint** - Updated to accept and forward documentData to email wrapper
45
+ - Receives documentData from preview component
46
+ - Passes through to convertToEmailSafeHtml function
47
+ - Maintains backward compatibility with existing implementations
48
+
49
+ - **Email Wrapper Interface** - Enhanced type definitions for better developer experience
50
+ - Added `EmailWrapperOptions` interface with documentData support
51
+ - Updated `CustomEmailWrapper` type for consistency
52
+ - Generic `Record<string, any>` allows any custom fields
53
+
54
+ - **Broadcast Sync Hooks** - Now apply wrapper during actual email sends
55
+ - Create operation uses wrapper when syncing to provider
56
+ - Update operation maintains wrapper consistency
57
+ - Deferred creates (from draft to published) include wrapper
58
+ - Same HTML template used for both preview and actual sends
59
+
60
+ ### Technical Changes
61
+ - Updated `convertToEmailSafeHtml` to accept documentData in options
62
+ - Modified all sync operations in Broadcasts collection to use email preview config
63
+ - Wrapper customization now available throughout the email lifecycle
64
+ - No breaking changes - wrapper remains optional with sensible defaults
65
+
66
+ ### Benefits
67
+ - **Flexibility** - Users can pass any custom fields without plugin modifications
68
+ - **Consistency** - Identical HTML in preview and actual email sends
69
+ - **Future Proof** - Add new fields to broadcasts without updating plugin code
70
+ - **No Breaking Changes** - Existing implementations continue to work unchanged
71
+
1
72
  ## [0.20.6] - 2025-08-01
2
73
 
3
74
  ### Fixed
package/README.md CHANGED
@@ -17,7 +17,7 @@ A complete newsletter management plugin for [Payload CMS](https://github.com/pay
17
17
  - 🌍 **Internationalization** - Multi-language support built-in
18
18
  - 📊 **Analytics Ready** - UTM tracking and signup metadata collection
19
19
  - ⚙️ **Admin UI Configuration** - Manage email settings through Payload admin panel
20
- - 🔄 **Bidirectional Sync** - Sync unsubscribes from email services back to Payload
20
+ - 🔄 **Real-time Webhook Sync** - Receive subscriber and broadcast events from email services via webhooks
21
21
  - 👁️ **Email Preview** - Real-time preview with desktop/mobile views (v0.9.0+)
22
22
  - ✅ **Email Validation** - Built-in validation for email client compatibility (v0.9.0+)
23
23
  - 📝 **Email-Safe Editor** - Rich text editor limited to email-compatible features (v0.9.0+)
@@ -654,27 +654,44 @@ This adds a "Newsletter Scheduling" group to your articles with:
654
654
  - Audience segment selection
655
655
  - Send status tracking
656
656
 
657
- ## Unsubscribe Sync
657
+ ## Webhook Configuration (Broadcast)
658
658
 
659
- The plugin supports bidirectional synchronization of unsubscribe states between Payload and your email service:
659
+ The plugin supports real-time webhook integration with Broadcast for instant updates:
660
660
 
661
- ```typescript
662
- features: {
663
- unsubscribeSync: {
664
- enabled: true,
665
- schedule: '0 * * * *', // Hourly sync
666
- queue: 'newsletter-sync' // Optional custom queue name
667
- }
668
- }
669
- ```
661
+ ### Automatic Updates
662
+
663
+ When configured, the plugin automatically receives and processes:
664
+ - **Subscriber Events**: `subscribed`, `unsubscribed`
665
+ - **Broadcast Events**: All status changes (`scheduled`, `in_progress`, `sent`, etc.)
666
+
667
+ ### Setup Instructions
668
+
669
+ 1. **Save your Newsletter Settings** in the Payload admin to generate a webhook URL
670
+ 2. **Configure in Broadcast**:
671
+ - Go to your Broadcast dashboard → "Webhook Endpoints"
672
+ - Click "Add Webhook Endpoint"
673
+ - Paste the webhook URL from Payload
674
+ - Select events:
675
+ - Subscriber Events: `subscribed`, `unsubscribed`
676
+ - Broadcast Events: All
677
+ - Create the webhook and copy the webhook secret
678
+ 3. **Add the webhook secret** to your Newsletter Settings in Payload
679
+ 4. **Save and verify** the webhook connection
680
+
681
+ ### Security
682
+
683
+ Webhooks are secured with:
684
+ - HMAC-SHA256 signature verification
685
+ - Timestamp validation (5-minute window)
686
+ - Secret key stored in Newsletter Settings
687
+
688
+ ### Data Flow
670
689
 
671
- This feature:
672
- - Polls your email service for unsubscribed users
673
- - Updates their status in Payload automatically
674
- - Supports both Broadcast and Resend providers
675
- - Can run on a schedule or be triggered manually
690
+ - **Subscriber events** update the subscriber's status and metadata
691
+ - **Broadcast events** update the broadcast's status and delivery metrics
692
+ - All updates happen in real-time without polling
676
693
 
677
- For more details, see the [Unsubscribe Sync documentation](./docs/unsubscribe-sync.md).
694
+ **Note**: Email engagement events (opens, clicks) remain in Broadcast for analytics.
678
695
 
679
696
  ## Email Providers
680
697
 
package/dist/admin.d.ts CHANGED
@@ -22,4 +22,6 @@ interface EmailPreviewProps {
22
22
  }
23
23
  declare const EmailPreview: React.FC<EmailPreviewProps>;
24
24
 
25
- export { BroadcastInlinePreview, type BroadcastInlinePreviewProps, EmailPreview, type EmailPreviewProps, StatusBadge, type StatusBadgeProps };
25
+ declare const WebhookConfiguration: React.FC;
26
+
27
+ export { BroadcastInlinePreview, type BroadcastInlinePreviewProps, EmailPreview, type EmailPreviewProps, StatusBadge, type StatusBadgeProps, WebhookConfiguration };
package/dist/admin.js CHANGED
@@ -22,10 +22,20 @@ var BroadcastInlinePreview = () => {
22
22
  setLoading(false);
23
23
  return;
24
24
  }
25
+ const documentData = {};
26
+ Object.entries(fields || {}).forEach(([key, field]) => {
27
+ if (field && typeof field === "object" && "value" in field) {
28
+ documentData[key] = field.value;
29
+ }
30
+ });
25
31
  const response = await fetch("/api/broadcasts/preview", {
26
32
  method: "POST",
27
33
  headers: { "Content-Type": "application/json" },
28
- body: JSON.stringify({ content: contentValue })
34
+ body: JSON.stringify({
35
+ content: contentValue,
36
+ documentData
37
+ // Pass all form data
38
+ })
29
39
  });
30
40
  if (!response.ok) {
31
41
  throw new Error(`Preview failed: ${response.statusText}`);
@@ -178,8 +188,153 @@ var EmailPreview = ({
178
188
  ] })
179
189
  ] });
180
190
  };
191
+
192
+ // src/admin/components/WebhookConfiguration.tsx
193
+ import { useState as useState2 } from "react";
194
+ import { useFormFields as useFormFields2 } from "@payloadcms/ui";
195
+ import { Button } from "@payloadcms/ui";
196
+ import { toast } from "@payloadcms/ui";
197
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
198
+ var WebhookConfiguration = () => {
199
+ const [showInstructions, setShowInstructions] = useState2(false);
200
+ const [verifying, setVerifying] = useState2(false);
201
+ const fields = useFormFields2(([fields2]) => ({
202
+ webhookUrl: fields2?.broadcastSettings?.webhookUrl,
203
+ webhookStatus: fields2?.broadcastSettings?.webhookStatus,
204
+ lastWebhookReceived: fields2?.broadcastSettings?.lastWebhookReceived
205
+ }));
206
+ const handleVerify = async () => {
207
+ setVerifying(true);
208
+ try {
209
+ const response = await fetch("/api/newsletter/webhooks/verify", {
210
+ method: "POST",
211
+ headers: {
212
+ "Content-Type": "application/json"
213
+ }
214
+ });
215
+ const data = await response.json();
216
+ if (data.success) {
217
+ toast.success(data.message || "Webhook verified");
218
+ } else {
219
+ toast.error(data.error || "Verification failed");
220
+ }
221
+ } catch (error) {
222
+ toast.error("Failed to verify webhook");
223
+ } finally {
224
+ setVerifying(false);
225
+ }
226
+ };
227
+ const getStatusColor = (status) => {
228
+ switch (status) {
229
+ case "verified":
230
+ return "green";
231
+ case "configured":
232
+ return "yellow";
233
+ case "error":
234
+ return "red";
235
+ default:
236
+ return "gray";
237
+ }
238
+ };
239
+ const getStatusLabel = (status) => {
240
+ switch (status) {
241
+ case "verified":
242
+ return "Verified \u2713";
243
+ case "configured":
244
+ return "Configured";
245
+ case "error":
246
+ return "Error";
247
+ default:
248
+ return "Not Configured";
249
+ }
250
+ };
251
+ return /* @__PURE__ */ jsx4("div", { className: "field-type", children: /* @__PURE__ */ jsxs3("div", { style: { marginBottom: "1rem" }, children: [
252
+ /* @__PURE__ */ jsx4("label", { className: "field-label", children: "Webhook Configuration" }),
253
+ /* @__PURE__ */ jsxs3("div", { style: {
254
+ background: "#f5f5f5",
255
+ padding: "1rem",
256
+ borderRadius: "4px",
257
+ marginTop: "0.5rem"
258
+ }, children: [
259
+ /* @__PURE__ */ jsxs3("div", { style: { marginBottom: "0.5rem" }, children: [
260
+ /* @__PURE__ */ jsx4("strong", { children: "Webhook URL:" }),
261
+ /* @__PURE__ */ jsx4("code", { style: {
262
+ display: "block",
263
+ padding: "0.5rem",
264
+ background: "#fff",
265
+ border: "1px solid #ddd",
266
+ borderRadius: "4px",
267
+ marginTop: "0.25rem",
268
+ fontSize: "0.875rem"
269
+ }, children: fields.webhookUrl || "Save settings to generate URL" })
270
+ ] }),
271
+ /* @__PURE__ */ jsxs3("div", { style: { marginBottom: "0.5rem" }, children: [
272
+ /* @__PURE__ */ jsx4("strong", { children: "Status:" }),
273
+ " ",
274
+ /* @__PURE__ */ jsx4("span", { style: { color: getStatusColor(fields.webhookStatus || "not_configured") }, children: getStatusLabel(fields.webhookStatus || "not_configured") })
275
+ ] }),
276
+ fields.lastWebhookReceived && /* @__PURE__ */ jsxs3("div", { style: { fontSize: "0.875rem", color: "#666" }, children: [
277
+ "Last event: ",
278
+ new Date(fields.lastWebhookReceived).toLocaleString()
279
+ ] })
280
+ ] }),
281
+ /* @__PURE__ */ jsxs3("div", { style: { marginTop: "1rem", display: "flex", gap: "0.5rem" }, children: [
282
+ /* @__PURE__ */ jsxs3(
283
+ Button,
284
+ {
285
+ onClick: () => setShowInstructions(!showInstructions),
286
+ buttonStyle: "secondary",
287
+ size: "small",
288
+ children: [
289
+ showInstructions ? "Hide" : "Show",
290
+ " Instructions"
291
+ ]
292
+ }
293
+ ),
294
+ /* @__PURE__ */ jsx4(
295
+ Button,
296
+ {
297
+ onClick: handleVerify,
298
+ buttonStyle: "primary",
299
+ size: "small",
300
+ disabled: verifying || !fields.webhookUrl,
301
+ children: verifying ? "Verifying..." : "Verify Webhook"
302
+ }
303
+ )
304
+ ] }),
305
+ showInstructions && /* @__PURE__ */ jsxs3("div", { style: {
306
+ marginTop: "1rem",
307
+ padding: "1rem",
308
+ background: "#f0f8ff",
309
+ border: "1px solid #b0d4ff",
310
+ borderRadius: "4px"
311
+ }, children: [
312
+ /* @__PURE__ */ jsx4("h4", { style: { marginTop: 0 }, children: "Configure Broadcast Webhook" }),
313
+ /* @__PURE__ */ jsxs3("ol", { children: [
314
+ /* @__PURE__ */ jsx4("li", { children: "Copy the webhook URL above" }),
315
+ /* @__PURE__ */ jsx4("li", { children: "Go to your Broadcast dashboard" }),
316
+ /* @__PURE__ */ jsx4("li", { children: 'Navigate to "Webhook Endpoints"' }),
317
+ /* @__PURE__ */ jsx4("li", { children: 'Click "Add Webhook Endpoint"' }),
318
+ /* @__PURE__ */ jsx4("li", { children: "Paste the URL" }),
319
+ /* @__PURE__ */ jsxs3("li", { children: [
320
+ "Select these events:",
321
+ /* @__PURE__ */ jsxs3("ul", { children: [
322
+ /* @__PURE__ */ jsx4("li", { children: "Subscriber Events: subscribed, unsubscribed" }),
323
+ /* @__PURE__ */ jsx4("li", { children: "Broadcast Events: All" })
324
+ ] })
325
+ ] }),
326
+ /* @__PURE__ */ jsx4("li", { children: 'Click "Create Webhook"' }),
327
+ /* @__PURE__ */ jsx4("li", { children: "Copy the webhook secret shown" }),
328
+ /* @__PURE__ */ jsx4("li", { children: "Paste it in the Webhook Secret field below" }),
329
+ /* @__PURE__ */ jsx4("li", { children: "Save these settings" }),
330
+ /* @__PURE__ */ jsx4("li", { children: 'Click "Verify Webhook" to test the connection' })
331
+ ] })
332
+ ] })
333
+ ] }) });
334
+ };
181
335
  export {
182
336
  BroadcastInlinePreview,
183
337
  EmailPreview,
184
- StatusBadge
338
+ StatusBadge,
339
+ WebhookConfiguration
185
340
  };
@@ -979,7 +979,8 @@ async function convertToEmailSafeHtml(editorState, options) {
979
979
  if (options.customWrapper) {
980
980
  return await Promise.resolve(options.customWrapper(sanitizedHtml, {
981
981
  preheader: options.preheader,
982
- subject: options.subject
982
+ subject: options.subject,
983
+ documentData: options.documentData
983
984
  }));
984
985
  }
985
986
  return wrapInEmailTemplate(sanitizedHtml, options.preheader);
@@ -1866,7 +1867,7 @@ var createBroadcastPreviewEndpoint = (config, _collectionSlug) => {
1866
1867
  handler: async (req) => {
1867
1868
  try {
1868
1869
  const data = await (req.json?.() || Promise.resolve({}));
1869
- const { content, preheader, subject } = data;
1870
+ const { content, preheader, subject, documentData } = data;
1870
1871
  if (!content) {
1871
1872
  return Response.json({
1872
1873
  success: false,
@@ -1882,6 +1883,8 @@ var createBroadcastPreviewEndpoint = (config, _collectionSlug) => {
1882
1883
  preheader,
1883
1884
  subject,
1884
1885
  mediaUrl,
1886
+ documentData,
1887
+ // Pass all document data
1885
1888
  customBlockConverter: config.customizations?.broadcasts?.customBlockConverter,
1886
1889
  customWrapper: emailPreviewConfig?.customWrapper
1887
1890
  });
@@ -2151,6 +2154,14 @@ var createBroadcastsCollection = (pluginConfig) => {
2151
2154
  condition: (data) => hasProviders && data?.providerId
2152
2155
  }
2153
2156
  },
2157
+ {
2158
+ name: "externalId",
2159
+ type: "text",
2160
+ admin: {
2161
+ readOnly: true,
2162
+ description: "External ID for webhook integration"
2163
+ }
2164
+ },
2154
2165
  {
2155
2166
  name: "providerData",
2156
2167
  type: "json",
@@ -2159,6 +2170,77 @@ var createBroadcastsCollection = (pluginConfig) => {
2159
2170
  condition: () => false
2160
2171
  // Hidden by default
2161
2172
  }
2173
+ },
2174
+ // Webhook tracking fields
2175
+ {
2176
+ name: "webhookData",
2177
+ type: "group",
2178
+ label: "Webhook Data",
2179
+ admin: {
2180
+ condition: () => false
2181
+ // Hidden by default, used for webhook tracking
2182
+ },
2183
+ fields: [
2184
+ {
2185
+ name: "lastWebhookEvent",
2186
+ type: "text",
2187
+ admin: {
2188
+ readOnly: true
2189
+ }
2190
+ },
2191
+ {
2192
+ name: "lastWebhookEventAt",
2193
+ type: "date",
2194
+ admin: {
2195
+ readOnly: true
2196
+ }
2197
+ },
2198
+ {
2199
+ name: "hasWarnings",
2200
+ type: "checkbox",
2201
+ defaultValue: false
2202
+ },
2203
+ {
2204
+ name: "failureReason",
2205
+ type: "text"
2206
+ },
2207
+ {
2208
+ name: "sentCount",
2209
+ type: "number"
2210
+ },
2211
+ {
2212
+ name: "totalCount",
2213
+ type: "number"
2214
+ },
2215
+ {
2216
+ name: "failedCount",
2217
+ type: "number"
2218
+ },
2219
+ {
2220
+ name: "remainingCount",
2221
+ type: "number"
2222
+ },
2223
+ {
2224
+ name: "sendingStartedAt",
2225
+ type: "date"
2226
+ },
2227
+ {
2228
+ name: "failedAt",
2229
+ type: "date"
2230
+ },
2231
+ {
2232
+ name: "abortedAt",
2233
+ type: "date"
2234
+ },
2235
+ {
2236
+ name: "abortReason",
2237
+ type: "text"
2238
+ },
2239
+ {
2240
+ name: "pausedAt",
2241
+ type: "date"
2242
+ }
2243
+ ]
2162
2244
  }
2163
2245
  ],
2164
2246
  hooks: {
@@ -2181,7 +2263,14 @@ var createBroadcastsCollection = (pluginConfig) => {
2181
2263
  const provider = new BroadcastApiProvider2(providerConfig);
2182
2264
  req.payload.logger.info("Populating media fields and converting content to HTML...");
2183
2265
  const populatedContent = await populateMediaFields(doc.contentSection?.content, req.payload, pluginConfig);
2266
+ const emailPreviewConfig = pluginConfig.customizations?.broadcasts?.emailPreview;
2184
2267
  const htmlContent = await convertToEmailSafeHtml(populatedContent, {
2268
+ wrapInTemplate: emailPreviewConfig?.wrapInTemplate ?? true,
2269
+ customWrapper: emailPreviewConfig?.customWrapper,
2270
+ preheader: doc.contentSection?.preheader,
2271
+ subject: doc.subject,
2272
+ documentData: doc,
2273
+ // Pass entire document
2185
2274
  customBlockConverter: pluginConfig.customizations?.broadcasts?.customBlockConverter
2186
2275
  });
2187
2276
  if (!htmlContent || htmlContent.trim() === "") {
@@ -2281,7 +2370,14 @@ var createBroadcastsCollection = (pluginConfig) => {
2281
2370
  }
2282
2371
  req.payload.logger.info("Creating broadcast in provider (deferred from initial create)...");
2283
2372
  const populatedContent = await populateMediaFields(doc.contentSection?.content, req.payload, pluginConfig);
2373
+ const emailPreviewConfig = pluginConfig.customizations?.broadcasts?.emailPreview;
2284
2374
  const htmlContent = await convertToEmailSafeHtml(populatedContent, {
2375
+ wrapInTemplate: emailPreviewConfig?.wrapInTemplate ?? true,
2376
+ customWrapper: emailPreviewConfig?.customWrapper,
2377
+ preheader: doc.contentSection?.preheader,
2378
+ subject: doc.subject,
2379
+ documentData: doc,
2380
+ // Pass entire document
2285
2381
  customBlockConverter: pluginConfig.customizations?.broadcasts?.customBlockConverter
2286
2382
  });
2287
2383
  if (!htmlContent || htmlContent.trim() === "") {
@@ -2343,7 +2439,14 @@ var createBroadcastsCollection = (pluginConfig) => {
2343
2439
  }
2344
2440
  if (JSON.stringify(doc.contentSection?.content) !== JSON.stringify(previousDoc?.contentSection?.content)) {
2345
2441
  const populatedContent = await populateMediaFields(doc.contentSection?.content, req.payload, pluginConfig);
2442
+ const emailPreviewConfig = pluginConfig.customizations?.broadcasts?.emailPreview;
2346
2443
  updates.content = await convertToEmailSafeHtml(populatedContent, {
2444
+ wrapInTemplate: emailPreviewConfig?.wrapInTemplate ?? true,
2445
+ customWrapper: emailPreviewConfig?.customWrapper,
2446
+ preheader: doc.contentSection?.preheader,
2447
+ subject: doc.subject,
2448
+ documentData: doc,
2449
+ // Pass entire document
2347
2450
  customBlockConverter: pluginConfig.customizations?.broadcasts?.customBlockConverter
2348
2451
  });
2349
2452
  }
@@ -2779,6 +2882,15 @@ var createSubscribersCollection = (pluginConfig) => {
2779
2882
  type: "date",
2780
2883
  hidden: true
2781
2884
  },
2885
+ // External ID for webhook integration
2886
+ {
2887
+ name: "externalId",
2888
+ type: "text",
2889
+ admin: {
2890
+ description: "ID from email service provider",
2891
+ readOnly: true
2892
+ }
2893
+ },
2782
2894
  // Subscription status
2783
2895
  {
2784
2896
  name: "subscriptionStatus",
@@ -2794,6 +2906,14 @@ var createSubscribersCollection = (pluginConfig) => {
2794
2906
  description: "Current subscription status"
2795
2907
  }
2796
2908
  },
2909
+ {
2910
+ name: "subscribedAt",
2911
+ type: "date",
2912
+ admin: {
2913
+ description: "When the user subscribed",
2914
+ readOnly: true
2915
+ }
2916
+ },
2797
2917
  {
2798
2918
  name: "unsubscribedAt",
2799
2919
  type: "date",
@@ -2803,6 +2923,15 @@ var createSubscribersCollection = (pluginConfig) => {
2803
2923
  readOnly: true
2804
2924
  }
2805
2925
  },
2926
+ {
2927
+ name: "unsubscribeReason",
2928
+ type: "text",
2929
+ admin: {
2930
+ condition: (data) => data?.subscriptionStatus === "unsubscribed",
2931
+ description: "Reason for unsubscribing",
2932
+ readOnly: true
2933
+ }
2934
+ },
2806
2935
  // Email preferences
2807
2936
  {
2808
2937
  name: "emailPreferences",