payload-plugin-newsletter 0.25.9 → 0.25.12
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 +24 -0
- package/dist/collections.cjs +89 -95
- package/dist/collections.cjs.map +1 -1
- package/dist/collections.js +89 -95
- package/dist/collections.js.map +1 -1
- package/dist/fields.cjs +4 -38
- package/dist/fields.cjs.map +1 -1
- package/dist/fields.js +4 -38
- package/dist/fields.js.map +1 -1
- package/dist/server.js +89 -95
- package/dist/utils.cjs +2 -2
- package/dist/utils.cjs.map +1 -1
- package/dist/utils.js +2 -2
- package/dist/utils.js.map +1 -1
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -3350,25 +3350,8 @@ var createEmailSafeFeatures = (additionalBlocks) => {
|
|
|
3350
3350
|
ItalicFeature(),
|
|
3351
3351
|
UnderlineFeature(),
|
|
3352
3352
|
StrikethroughFeature(),
|
|
3353
|
-
// Links
|
|
3354
|
-
LinkFeature(
|
|
3355
|
-
fields: [
|
|
3356
|
-
{
|
|
3357
|
-
name: "url",
|
|
3358
|
-
type: "text",
|
|
3359
|
-
required: true,
|
|
3360
|
-
admin: {
|
|
3361
|
-
description: "Enter the full URL (including https://)"
|
|
3362
|
-
}
|
|
3363
|
-
},
|
|
3364
|
-
{
|
|
3365
|
-
name: "newTab",
|
|
3366
|
-
type: "checkbox",
|
|
3367
|
-
label: "Open in new tab",
|
|
3368
|
-
defaultValue: false
|
|
3369
|
-
}
|
|
3370
|
-
]
|
|
3371
|
-
}),
|
|
3353
|
+
// Links - use default fields to ensure drawer UI works correctly
|
|
3354
|
+
LinkFeature(),
|
|
3372
3355
|
// Lists
|
|
3373
3356
|
OrderedListFeature(),
|
|
3374
3357
|
UnorderedListFeature(),
|
|
@@ -3424,25 +3407,8 @@ var createEmailLexicalEditor = (customBlocks = []) => {
|
|
|
3424
3407
|
ItalicFeature(),
|
|
3425
3408
|
UnderlineFeature(),
|
|
3426
3409
|
StrikethroughFeature(),
|
|
3427
|
-
// Links
|
|
3428
|
-
LinkFeature(
|
|
3429
|
-
fields: [
|
|
3430
|
-
{
|
|
3431
|
-
name: "url",
|
|
3432
|
-
type: "text",
|
|
3433
|
-
required: true,
|
|
3434
|
-
admin: {
|
|
3435
|
-
description: "Enter the full URL (including https://)"
|
|
3436
|
-
}
|
|
3437
|
-
},
|
|
3438
|
-
{
|
|
3439
|
-
name: "newTab",
|
|
3440
|
-
type: "checkbox",
|
|
3441
|
-
label: "Open in new tab",
|
|
3442
|
-
defaultValue: false
|
|
3443
|
-
}
|
|
3444
|
-
]
|
|
3445
|
-
}),
|
|
3410
|
+
// Links - use default fields to ensure drawer UI works correctly
|
|
3411
|
+
LinkFeature(),
|
|
3446
3412
|
// Lists
|
|
3447
3413
|
OrderedListFeature(),
|
|
3448
3414
|
UnorderedListFeature(),
|
|
@@ -4010,7 +3976,7 @@ async function getBroadcastConfig(req, pluginConfig) {
|
|
|
4010
3976
|
}
|
|
4011
3977
|
return pluginConfig.providers?.broadcast || null;
|
|
4012
3978
|
} catch (error) {
|
|
4013
|
-
req.payload.logger.error("Failed to get broadcast config from settings
|
|
3979
|
+
req.payload.logger.error({ error: String(error) }, "Failed to get broadcast config from settings");
|
|
4014
3980
|
return pluginConfig.providers?.broadcast || null;
|
|
4015
3981
|
}
|
|
4016
3982
|
}
|
|
@@ -4323,70 +4289,88 @@ var createTestBroadcastEndpoint = (config, collectionSlug) => {
|
|
|
4323
4289
|
};
|
|
4324
4290
|
};
|
|
4325
4291
|
|
|
4326
|
-
// src/
|
|
4292
|
+
// src/utils/mediaPopulation.ts
|
|
4327
4293
|
async function populateMediaFields(content, payload, config) {
|
|
4328
4294
|
if (!content || typeof content !== "object") return content;
|
|
4329
|
-
|
|
4330
|
-
|
|
4295
|
+
const typedContent = content;
|
|
4296
|
+
if (typedContent.root?.children) {
|
|
4297
|
+
for (const child of typedContent.root.children) {
|
|
4331
4298
|
await populateBlockMediaFields(child, payload, config);
|
|
4332
4299
|
}
|
|
4333
4300
|
}
|
|
4334
4301
|
return content;
|
|
4335
4302
|
}
|
|
4336
4303
|
async function populateBlockMediaFields(node, payload, config) {
|
|
4337
|
-
if (node
|
|
4338
|
-
|
|
4304
|
+
if (!node || typeof node !== "object") return;
|
|
4305
|
+
const typedNode = node;
|
|
4306
|
+
if (typedNode.type === "block" && typedNode.fields) {
|
|
4307
|
+
const blockType = typedNode.fields.blockType || typedNode.fields.blockName;
|
|
4339
4308
|
const customBlocks = config.customizations?.broadcasts?.customBlocks || [];
|
|
4340
4309
|
const blockConfig = customBlocks.find((b) => b.slug === blockType);
|
|
4341
4310
|
if (blockConfig && blockConfig.fields) {
|
|
4342
4311
|
for (const field of blockConfig.fields) {
|
|
4343
|
-
if (field.type === "upload" && field.relationTo &&
|
|
4344
|
-
const fieldValue =
|
|
4312
|
+
if (field.type === "upload" && field.relationTo && typedNode.fields[field.name]) {
|
|
4313
|
+
const fieldValue = typedNode.fields[field.name];
|
|
4314
|
+
const collectionName = Array.isArray(field.relationTo) ? field.relationTo[0] : field.relationTo;
|
|
4345
4315
|
if (typeof fieldValue === "string" && fieldValue.match(/^[a-f0-9]{24}$/i)) {
|
|
4346
4316
|
try {
|
|
4347
4317
|
const media = await payload.findByID({
|
|
4348
|
-
collection:
|
|
4318
|
+
collection: collectionName,
|
|
4349
4319
|
id: fieldValue,
|
|
4350
4320
|
depth: 0
|
|
4351
4321
|
});
|
|
4352
4322
|
if (media) {
|
|
4353
|
-
|
|
4354
|
-
payload.logger?.info(
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4323
|
+
typedNode.fields[field.name] = media;
|
|
4324
|
+
payload.logger?.info(
|
|
4325
|
+
{
|
|
4326
|
+
mediaId: fieldValue,
|
|
4327
|
+
mediaUrl: media.url,
|
|
4328
|
+
filename: media.filename
|
|
4329
|
+
},
|
|
4330
|
+
`Populated ${field.name} for block ${blockType}`
|
|
4331
|
+
);
|
|
4359
4332
|
}
|
|
4360
4333
|
} catch (error) {
|
|
4361
|
-
payload.logger?.error(
|
|
4334
|
+
payload.logger?.error(
|
|
4335
|
+
{ error: String(error) },
|
|
4336
|
+
`Failed to populate ${field.name} for block ${blockType}`
|
|
4337
|
+
);
|
|
4362
4338
|
}
|
|
4363
4339
|
}
|
|
4364
4340
|
}
|
|
4365
4341
|
if (field.type === "array" && field.fields) {
|
|
4366
|
-
const arrayValue =
|
|
4342
|
+
const arrayValue = typedNode.fields[field.name];
|
|
4367
4343
|
if (Array.isArray(arrayValue)) {
|
|
4368
4344
|
for (const arrayItem of arrayValue) {
|
|
4369
4345
|
if (arrayItem && typeof arrayItem === "object") {
|
|
4346
|
+
const typedArrayItem = arrayItem;
|
|
4370
4347
|
for (const arrayField of field.fields) {
|
|
4371
|
-
if (arrayField.type === "upload" && arrayField.relationTo &&
|
|
4372
|
-
const arrayFieldValue =
|
|
4348
|
+
if (arrayField.type === "upload" && arrayField.relationTo && typedArrayItem[arrayField.name]) {
|
|
4349
|
+
const arrayFieldValue = typedArrayItem[arrayField.name];
|
|
4350
|
+
const arrayCollectionName = Array.isArray(arrayField.relationTo) ? arrayField.relationTo[0] : arrayField.relationTo;
|
|
4373
4351
|
if (typeof arrayFieldValue === "string" && arrayFieldValue.match(/^[a-f0-9]{24}$/i)) {
|
|
4374
4352
|
try {
|
|
4375
4353
|
const media = await payload.findByID({
|
|
4376
|
-
collection:
|
|
4354
|
+
collection: arrayCollectionName,
|
|
4377
4355
|
id: arrayFieldValue,
|
|
4378
4356
|
depth: 0
|
|
4379
4357
|
});
|
|
4380
4358
|
if (media) {
|
|
4381
|
-
|
|
4382
|
-
payload.logger?.info(
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4359
|
+
typedArrayItem[arrayField.name] = media;
|
|
4360
|
+
payload.logger?.info(
|
|
4361
|
+
{
|
|
4362
|
+
mediaId: arrayFieldValue,
|
|
4363
|
+
mediaUrl: media.url,
|
|
4364
|
+
filename: media.filename
|
|
4365
|
+
},
|
|
4366
|
+
`Populated array ${arrayField.name} for block ${blockType}`
|
|
4367
|
+
);
|
|
4387
4368
|
}
|
|
4388
4369
|
} catch (error) {
|
|
4389
|
-
payload.logger?.error(
|
|
4370
|
+
payload.logger?.error(
|
|
4371
|
+
{ error: String(error) },
|
|
4372
|
+
`Failed to populate array ${arrayField.name} for block ${blockType}`
|
|
4373
|
+
);
|
|
4390
4374
|
}
|
|
4391
4375
|
}
|
|
4392
4376
|
}
|
|
@@ -4395,23 +4379,24 @@ async function populateBlockMediaFields(node, payload, config) {
|
|
|
4395
4379
|
}
|
|
4396
4380
|
}
|
|
4397
4381
|
}
|
|
4398
|
-
if (field.type === "richText" &&
|
|
4399
|
-
await populateRichTextUploads(
|
|
4382
|
+
if (field.type === "richText" && typedNode.fields[field.name]) {
|
|
4383
|
+
await populateRichTextUploads(typedNode.fields[field.name], payload);
|
|
4400
4384
|
payload.logger?.info(`Processed rich text field ${field.name} for upload nodes`);
|
|
4401
4385
|
}
|
|
4402
4386
|
}
|
|
4403
4387
|
}
|
|
4404
4388
|
}
|
|
4405
|
-
if (
|
|
4406
|
-
for (const child of
|
|
4389
|
+
if (typedNode.children) {
|
|
4390
|
+
for (const child of typedNode.children) {
|
|
4407
4391
|
await populateBlockMediaFields(child, payload, config);
|
|
4408
4392
|
}
|
|
4409
4393
|
}
|
|
4410
4394
|
}
|
|
4411
4395
|
async function populateRichTextUploads(content, payload) {
|
|
4412
4396
|
if (!content || typeof content !== "object") return;
|
|
4413
|
-
|
|
4414
|
-
|
|
4397
|
+
const typedContent = content;
|
|
4398
|
+
if (typedContent.root?.children) {
|
|
4399
|
+
await processNodeArray(typedContent.root.children);
|
|
4415
4400
|
}
|
|
4416
4401
|
if (Array.isArray(content)) {
|
|
4417
4402
|
await processNodeArray(content);
|
|
@@ -4421,33 +4406,42 @@ async function populateRichTextUploads(content, payload) {
|
|
|
4421
4406
|
}
|
|
4422
4407
|
async function processNode(node) {
|
|
4423
4408
|
if (!node || typeof node !== "object") return;
|
|
4424
|
-
|
|
4409
|
+
const typedNode = node;
|
|
4410
|
+
if (typedNode.type === "upload" && typedNode.relationTo === "media" && typeof typedNode.value === "string" && typedNode.value.match(/^[a-f0-9]{24}$/i)) {
|
|
4425
4411
|
try {
|
|
4426
4412
|
const media = await payload.findByID({
|
|
4427
4413
|
collection: "media",
|
|
4428
|
-
id:
|
|
4414
|
+
id: typedNode.value,
|
|
4429
4415
|
depth: 0
|
|
4430
4416
|
});
|
|
4431
4417
|
if (media) {
|
|
4432
|
-
|
|
4433
|
-
payload.logger?.info(
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4418
|
+
typedNode.value = media;
|
|
4419
|
+
payload.logger?.info(
|
|
4420
|
+
{
|
|
4421
|
+
mediaId: typedNode.value,
|
|
4422
|
+
mediaUrl: media.url,
|
|
4423
|
+
filename: media.filename
|
|
4424
|
+
},
|
|
4425
|
+
"Populated rich text upload node"
|
|
4426
|
+
);
|
|
4438
4427
|
}
|
|
4439
4428
|
} catch (error) {
|
|
4440
|
-
payload.logger?.error(
|
|
4429
|
+
payload.logger?.error(
|
|
4430
|
+
{ error: String(error) },
|
|
4431
|
+
`Failed to populate rich text upload ${typedNode.value}`
|
|
4432
|
+
);
|
|
4441
4433
|
}
|
|
4442
4434
|
}
|
|
4443
|
-
if (
|
|
4444
|
-
await processNodeArray(
|
|
4435
|
+
if (typedNode.children && Array.isArray(typedNode.children)) {
|
|
4436
|
+
await processNodeArray(typedNode.children);
|
|
4445
4437
|
}
|
|
4446
|
-
if (
|
|
4447
|
-
await processNodeArray(
|
|
4438
|
+
if (typedNode.root?.children && Array.isArray(typedNode.root.children)) {
|
|
4439
|
+
await processNodeArray(typedNode.root.children);
|
|
4448
4440
|
}
|
|
4449
4441
|
}
|
|
4450
4442
|
}
|
|
4443
|
+
|
|
4444
|
+
// src/endpoints/broadcasts/preview.ts
|
|
4451
4445
|
var createBroadcastPreviewEndpoint = (config, _collectionSlug) => {
|
|
4452
4446
|
return {
|
|
4453
4447
|
path: "/preview",
|
|
@@ -4841,7 +4835,7 @@ var createBroadcastsCollection = (pluginConfig) => {
|
|
|
4841
4835
|
return doc;
|
|
4842
4836
|
}
|
|
4843
4837
|
if (operation === "update") {
|
|
4844
|
-
req.payload.logger.info(
|
|
4838
|
+
req.payload.logger.info({
|
|
4845
4839
|
operation,
|
|
4846
4840
|
hasProviderId: !!doc.providerId,
|
|
4847
4841
|
hasExternalId: !!doc.externalId,
|
|
@@ -4849,7 +4843,7 @@ var createBroadcastsCollection = (pluginConfig) => {
|
|
|
4849
4843
|
publishStatus: doc._status,
|
|
4850
4844
|
hasSubject: !!doc.subject,
|
|
4851
4845
|
hasContent: !!doc.contentSection?.content
|
|
4852
|
-
});
|
|
4846
|
+
}, "Broadcast afterChange update hook triggered");
|
|
4853
4847
|
try {
|
|
4854
4848
|
const providerConfig = await getBroadcastConfig(req, pluginConfig);
|
|
4855
4849
|
if (!providerConfig || !providerConfig.token) {
|
|
@@ -4936,10 +4930,10 @@ var createBroadcastsCollection = (pluginConfig) => {
|
|
|
4936
4930
|
if (JSON.stringify(doc.audienceIds) !== JSON.stringify(previousDoc?.audienceIds)) {
|
|
4937
4931
|
updates.audienceIds = doc.audienceIds?.map((a) => a.audienceId);
|
|
4938
4932
|
}
|
|
4939
|
-
req.payload.logger.info(
|
|
4933
|
+
req.payload.logger.info({
|
|
4940
4934
|
providerId: doc.providerId,
|
|
4941
4935
|
updates
|
|
4942
|
-
});
|
|
4936
|
+
}, "Syncing broadcast updates to provider");
|
|
4943
4937
|
await provider.update(doc.providerId, updates);
|
|
4944
4938
|
req.payload.logger.info(`Broadcast ${doc.id} synced to provider successfully`);
|
|
4945
4939
|
} else {
|
|
@@ -4961,18 +4955,18 @@ var createBroadcastsCollection = (pluginConfig) => {
|
|
|
4961
4955
|
...error.statusText
|
|
4962
4956
|
});
|
|
4963
4957
|
} else if (typeof error === "string") {
|
|
4964
|
-
req.payload.logger.error("Error is a string
|
|
4958
|
+
req.payload.logger.error({ errorValue: error }, "Error is a string");
|
|
4965
4959
|
} else if (error && typeof error === "object") {
|
|
4966
|
-
req.payload.logger.error(
|
|
4960
|
+
req.payload.logger.error({ errorValue: JSON.stringify(error, null, 2) }, "Error is an object");
|
|
4967
4961
|
} else {
|
|
4968
|
-
req.payload.logger.error("Unknown error type
|
|
4962
|
+
req.payload.logger.error({ errorType: typeof error }, "Unknown error type");
|
|
4969
4963
|
}
|
|
4970
|
-
req.payload.logger.error(
|
|
4964
|
+
req.payload.logger.error({
|
|
4971
4965
|
id: doc.id,
|
|
4972
4966
|
subject: doc.subject,
|
|
4973
4967
|
hasContent: !!doc.contentSection?.content,
|
|
4974
4968
|
contentType: doc.contentSection?.content ? typeof doc.contentSection.content : "none"
|
|
4975
|
-
});
|
|
4969
|
+
}, "Failed broadcast document (update operation)");
|
|
4976
4970
|
}
|
|
4977
4971
|
}
|
|
4978
4972
|
return doc;
|
|
@@ -5018,7 +5012,7 @@ var createBroadcastsCollection = (pluginConfig) => {
|
|
|
5018
5012
|
...error.details
|
|
5019
5013
|
});
|
|
5020
5014
|
} else {
|
|
5021
|
-
req.payload.logger.error(`Failed to send broadcast ${doc.id}
|
|
5015
|
+
req.payload.logger.error({ error: String(error) }, `Failed to send broadcast ${doc.id}`);
|
|
5022
5016
|
}
|
|
5023
5017
|
await req.payload.update({
|
|
5024
5018
|
collection: "broadcasts",
|
|
@@ -5061,7 +5055,7 @@ var createBroadcastsCollection = (pluginConfig) => {
|
|
|
5061
5055
|
...error.details
|
|
5062
5056
|
});
|
|
5063
5057
|
} else {
|
|
5064
|
-
req.payload.logger.error("Failed to delete broadcast from provider
|
|
5058
|
+
req.payload.logger.error({ error: String(error) }, "Failed to delete broadcast from provider");
|
|
5065
5059
|
}
|
|
5066
5060
|
}
|
|
5067
5061
|
return doc;
|
package/dist/utils.cjs
CHANGED
|
@@ -621,7 +621,7 @@ async function getBroadcastConfig(req, pluginConfig) {
|
|
|
621
621
|
}
|
|
622
622
|
return pluginConfig.providers?.broadcast || null;
|
|
623
623
|
} catch (error) {
|
|
624
|
-
req.payload.logger.error("Failed to get broadcast config from settings
|
|
624
|
+
req.payload.logger.error({ error: String(error) }, "Failed to get broadcast config from settings");
|
|
625
625
|
return pluginConfig.providers?.broadcast || null;
|
|
626
626
|
}
|
|
627
627
|
}
|
|
@@ -643,7 +643,7 @@ async function getResendConfig(req, pluginConfig) {
|
|
|
643
643
|
}
|
|
644
644
|
return pluginConfig.providers?.resend || null;
|
|
645
645
|
} catch (error) {
|
|
646
|
-
req.payload.logger.error("Failed to get resend config from settings
|
|
646
|
+
req.payload.logger.error({ error: String(error) }, "Failed to get resend config from settings");
|
|
647
647
|
return pluginConfig.providers?.resend || null;
|
|
648
648
|
}
|
|
649
649
|
}
|
package/dist/utils.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/exports/utils.ts","../src/utils/emailSafeHtml.ts","../src/utils/validateEmailHtml.ts","../src/utils/getBroadcastConfig.ts","../src/utils/getResendConfig.ts"],"sourcesContent":["// Email utilities\nexport { convertToEmailSafeHtml, EMAIL_SAFE_CONFIG } from '../utils/emailSafeHtml'\nexport { validateEmailHtml } from '../utils/validateEmailHtml'\nexport type { ValidationResult } from '../utils/validateEmailHtml'\n\n// Configuration utilities\nexport { getBroadcastConfig } from '../utils/getBroadcastConfig'\nexport { getResendConfig } from '../utils/getResendConfig'","import DOMPurify from 'isomorphic-dompurify'\nimport type { SerializedEditorState } from 'lexical'\n\n/**\n * DOMPurify configuration for email-safe HTML\n */\nexport const EMAIL_SAFE_CONFIG = {\n ALLOWED_TAGS: [\n 'p', 'br', 'strong', 'b', 'em', 'i', 'u', 'strike', 's', 'span',\n 'a', 'h1', 'h2', 'h3', 'ul', 'ol', 'li', 'blockquote', 'hr',\n 'img', 'div', 'table', 'tr', 'td', 'th', 'tbody', 'thead'\n ],\n ALLOWED_ATTR: ['href', 'style', 'target', 'rel', 'align', 'src', 'alt', 'width', 'height', 'border', 'cellpadding', 'cellspacing'],\n ALLOWED_STYLES: {\n '*': [\n 'color', 'background-color', 'font-size', 'font-weight',\n 'font-style', 'text-decoration', 'text-align', 'margin',\n 'margin-top', 'margin-right', 'margin-bottom', 'margin-left',\n 'padding', 'padding-top', 'padding-right', 'padding-bottom', \n 'padding-left', 'line-height', 'border-left', 'border-left-width',\n 'border-left-style', 'border-left-color'\n ],\n },\n FORBID_TAGS: ['script', 'style', 'iframe', 'object', 'embed', 'form', 'input'],\n FORBID_ATTR: ['class', 'id', 'onclick', 'onload', 'onerror'],\n}\n\n/**\n * Converts Lexical editor state to email-safe HTML\n */\nexport async function convertToEmailSafeHtml(\n editorState: SerializedEditorState | undefined | null,\n options?: {\n wrapInTemplate?: boolean\n preheader?: string\n mediaUrl?: string // Base URL for media files\n customBlockConverter?: (node: any, mediaUrl?: string) => Promise<string>\n payload?: any // Payload instance for populating relationships\n populateFields?: string[] | ((blockType: string) => string[]) // Fields to populate\n customWrapper?: (content: string, options?: { preheader?: string; subject?: string; documentData?: Record<string, any> }) => string | Promise<string>\n subject?: string // Email subject for custom wrapper\n documentData?: Record<string, any> // Generic document data for custom wrapper\n }\n): Promise<string> {\n // Handle empty content\n if (!editorState) {\n return ''\n }\n \n // First, convert Lexical state to HTML using custom converters\n const rawHtml = await lexicalToEmailHtml(editorState, options?.mediaUrl, options?.customBlockConverter)\n \n // Sanitize the HTML\n const sanitizedHtml = DOMPurify.sanitize(rawHtml, EMAIL_SAFE_CONFIG)\n \n // Optionally wrap in email template\n if (options?.wrapInTemplate) {\n if (options.customWrapper) {\n return await Promise.resolve(options.customWrapper(sanitizedHtml, { \n preheader: options.preheader,\n subject: options.subject,\n documentData: options.documentData\n }))\n }\n return wrapInEmailTemplate(sanitizedHtml, options.preheader)\n }\n \n return sanitizedHtml\n}\n\n/**\n * Custom Lexical to HTML converter for email\n */\nasync function lexicalToEmailHtml(\n editorState: SerializedEditorState, \n mediaUrl?: string,\n customBlockConverter?: (node: any, mediaUrl?: string) => Promise<string>\n): Promise<string> {\n const { root } = editorState\n \n if (!root || !root.children) {\n return ''\n }\n \n // Convert nodes asynchronously to support custom converters\n const htmlParts = await Promise.all(\n root.children.map((node: any) => convertNode(node, mediaUrl, customBlockConverter))\n )\n \n return htmlParts.join('')\n}\n\n/**\n * Convert individual Lexical nodes to email-safe HTML\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nasync function convertNode(\n node: any, \n mediaUrl?: string,\n customBlockConverter?: (node: any, mediaUrl?: string) => Promise<string>\n): Promise<string> {\n switch (node.type) {\n case 'paragraph':\n return convertParagraph(node, mediaUrl, customBlockConverter)\n case 'heading':\n return convertHeading(node, mediaUrl, customBlockConverter)\n case 'list':\n return convertList(node, mediaUrl, customBlockConverter)\n case 'listitem':\n return convertListItem(node, mediaUrl, customBlockConverter)\n case 'blockquote':\n return convertBlockquote(node, mediaUrl, customBlockConverter)\n case 'text':\n return convertText(node)\n case 'link':\n return convertLink(node, mediaUrl, customBlockConverter)\n case 'linebreak':\n return '<br>'\n case 'upload':\n return convertUpload(node, mediaUrl)\n case 'block':\n return await convertBlock(node, mediaUrl, customBlockConverter)\n default:\n // Unknown node type - convert children if any\n if (node.children) {\n const childParts = await Promise.all(\n node.children.map((child: any) => convertNode(child, mediaUrl, customBlockConverter))\n )\n return childParts.join('')\n }\n return ''\n }\n}\n\n/**\n * Convert paragraph node\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nasync function convertParagraph(\n node: any, \n mediaUrl?: string,\n customBlockConverter?: (node: any, mediaUrl?: string) => Promise<string>\n): Promise<string> {\n const align = getAlignment(node.format)\n const childParts = await Promise.all(\n (node.children || []).map((child: any) => convertNode(child, mediaUrl, customBlockConverter))\n )\n const children = childParts.join('')\n \n if (!children.trim()) {\n return '<p class=\"mobile-margin-bottom-16\" style=\"margin: 0 0 16px 0; min-height: 1em;\"> </p>'\n }\n \n return `<p class=\"mobile-margin-bottom-16\" style=\"margin: 0 0 16px 0; text-align: ${align}; font-size: 16px; line-height: 1.5;\">${children}</p>`\n}\n\n/**\n * Convert heading node\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nasync function convertHeading(\n node: any, \n mediaUrl?: string,\n customBlockConverter?: (node: any, mediaUrl?: string) => Promise<string>\n): Promise<string> {\n const tag = node.tag || 'h1'\n const align = getAlignment(node.format)\n const childParts = await Promise.all(\n (node.children || []).map((child: any) => convertNode(child, mediaUrl, customBlockConverter))\n )\n const children = childParts.join('')\n \n const styles: Record<string, string> = {\n h1: 'font-size: 32px; font-weight: 700; margin: 0 0 24px 0; line-height: 1.2;',\n h2: 'font-size: 24px; font-weight: 600; margin: 0 0 16px 0; line-height: 1.3;',\n h3: 'font-size: 20px; font-weight: 600; margin: 0 0 12px 0; line-height: 1.4;',\n }\n \n const mobileClasses: Record<string, string> = {\n h1: 'mobile-font-size-24',\n h2: 'mobile-font-size-20',\n h3: 'mobile-font-size-16',\n }\n \n const style = `${styles[tag] || styles.h3} text-align: ${align};`\n const mobileClass = mobileClasses[tag] || mobileClasses.h3\n \n return `<${tag} class=\"${mobileClass}\" style=\"${style}\">${children}</${tag}>`\n}\n\n/**\n * Convert list node\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nasync function convertList(\n node: any, \n mediaUrl?: string,\n customBlockConverter?: (node: any, mediaUrl?: string) => Promise<string>\n): Promise<string> {\n const tag = node.listType === 'number' ? 'ol' : 'ul'\n const childParts = await Promise.all(\n (node.children || []).map((child: any) => convertNode(child, mediaUrl, customBlockConverter))\n )\n const children = childParts.join('')\n \n const style = tag === 'ul' \n ? 'margin: 0 0 16px 0; padding-left: 24px; list-style-type: disc; font-size: 16px; line-height: 1.5;'\n : 'margin: 0 0 16px 0; padding-left: 24px; list-style-type: decimal; font-size: 16px; line-height: 1.5;'\n \n return `<${tag} class=\"mobile-margin-bottom-16\" style=\"${style}\">${children}</${tag}>`\n}\n\n/**\n * Convert list item node\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nasync function convertListItem(\n node: any, \n mediaUrl?: string,\n customBlockConverter?: (node: any, mediaUrl?: string) => Promise<string>\n): Promise<string> {\n const childParts = await Promise.all(\n (node.children || []).map((child: any) => convertNode(child, mediaUrl, customBlockConverter))\n )\n const children = childParts.join('')\n return `<li style=\"margin: 0 0 8px 0;\">${children}</li>`\n}\n\n/**\n * Convert blockquote node\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nasync function convertBlockquote(\n node: any, \n mediaUrl?: string,\n customBlockConverter?: (node: any, mediaUrl?: string) => Promise<string>\n): Promise<string> {\n const childParts = await Promise.all(\n (node.children || []).map((child: any) => convertNode(child, mediaUrl, customBlockConverter))\n )\n const children = childParts.join('')\n const style = 'margin: 0 0 16px 0; padding-left: 16px; border-left: 4px solid #e5e7eb; color: #6b7280;'\n \n return `<blockquote style=\"${style}\">${children}</blockquote>`\n}\n\n/**\n * Convert text node\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction convertText(node: any): string {\n let text = escapeHtml(node.text || '')\n \n // Apply formatting\n if (node.format & 1) { // Bold\n text = `<strong>${text}</strong>`\n }\n if (node.format & 2) { // Italic\n text = `<em>${text}</em>`\n }\n if (node.format & 8) { // Underline\n text = `<u>${text}</u>`\n }\n if (node.format & 4) { // Strikethrough\n text = `<strike>${text}</strike>`\n }\n \n return text\n}\n\n/**\n * Convert link node\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nasync function convertLink(\n node: any, \n mediaUrl?: string,\n customBlockConverter?: (node: any, mediaUrl?: string) => Promise<string>\n): Promise<string> {\n const childParts = await Promise.all(\n (node.children || []).map((child: any) => convertNode(child, mediaUrl, customBlockConverter))\n )\n const children = childParts.join('')\n const url = node.fields?.url || '#'\n const newTab = node.fields?.newTab ?? false\n \n // Add target and rel attributes based on newTab setting\n const targetAttr = newTab ? ' target=\"_blank\"' : ''\n const relAttr = newTab ? ' rel=\"noopener noreferrer\"' : ''\n \n return `<a href=\"${escapeHtml(url)}\"${targetAttr}${relAttr} style=\"color: #2563eb; text-decoration: underline;\">${children}</a>`\n}\n\n/**\n * Convert upload (image) node\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction convertUpload(node: any, mediaUrl?: string): string {\n const upload = node.value\n if (!upload) return ''\n \n // Get image URL - handle both direct URL and media object\n let src = ''\n if (typeof upload === 'string') {\n src = upload\n } else if (upload.url) {\n src = upload.url\n } else if (upload.filename && mediaUrl) {\n // Construct URL from media URL and filename\n src = `${mediaUrl}/${upload.filename}`\n }\n \n const alt = node.fields?.altText || upload.alt || ''\n const caption = node.fields?.caption || ''\n \n // Responsive email-safe image\n const imgHtml = `<img src=\"${escapeHtml(src)}\" alt=\"${escapeHtml(alt)}\" class=\"mobile-width-100\" style=\"max-width: 100%; height: auto; display: block; margin: 0 auto; border-radius: 6px;\" />`\n \n if (caption) {\n return `\n <div style=\"margin: 0 0 16px 0; text-align: center;\" class=\"mobile-margin-bottom-16\">\n ${imgHtml}\n <p style=\"margin: 8px 0 0 0; font-size: 14px; color: #6b7280; font-style: italic; text-align: center;\" class=\"mobile-font-size-14\">${escapeHtml(caption)}</p>\n </div>\n `\n }\n \n return `<div style=\"margin: 0 0 16px 0; text-align: center;\" class=\"mobile-margin-bottom-16\">${imgHtml}</div>`\n}\n\n/**\n * Convert custom block node\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nasync function convertBlock(\n node: any, \n mediaUrl?: string,\n customBlockConverter?: (node: any, mediaUrl?: string) => Promise<string>\n): Promise<string> {\n const blockType = node.fields?.blockName || node.blockName\n \n // First, check if there's a custom converter for this block\n if (customBlockConverter) {\n try {\n const customHtml = await customBlockConverter(node, mediaUrl)\n if (customHtml) {\n return customHtml\n }\n } catch (error) {\n console.error(`Custom block converter error for ${blockType}:`, error)\n // Fall through to default handling\n }\n }\n \n // Default handling for built-in blocks\n switch (blockType) {\n case 'button':\n return convertButtonBlock(node.fields)\n case 'divider':\n return convertDividerBlock(node.fields)\n default:\n // Unknown block type - try to convert children\n if (node.children) {\n const childParts = await Promise.all(\n node.children.map((child: any) => convertNode(child, mediaUrl, customBlockConverter))\n )\n return childParts.join('')\n }\n return ''\n }\n}\n\n/**\n * Convert button block\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction convertButtonBlock(fields: any): string {\n const text = fields?.text || 'Click here'\n const url = fields?.url || '#'\n const style = fields?.style || 'primary'\n \n const styles: Record<string, string> = {\n primary: 'background-color: #2563eb; color: #ffffff; border: 2px solid #2563eb;',\n secondary: 'background-color: #6b7280; color: #ffffff; border: 2px solid #6b7280;',\n outline: 'background-color: transparent; color: #2563eb; border: 2px solid #2563eb;',\n }\n \n const buttonStyle = `${styles[style] || styles.primary} display: inline-block; padding: 12px 24px; font-size: 16px; font-weight: 600; text-decoration: none; border-radius: 6px; text-align: center;`\n \n return `\n <div style=\"margin: 0 0 16px 0; text-align: center;\">\n <a href=\"${escapeHtml(url)}\" target=\"_blank\" rel=\"noopener noreferrer\" style=\"${buttonStyle}\">${escapeHtml(text)}</a>\n </div>\n `\n}\n\n/**\n * Convert divider block\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction convertDividerBlock(fields: any): string {\n const style = fields?.style || 'solid'\n \n const styles: Record<string, string> = {\n solid: 'border-top: 1px solid #e5e7eb;',\n dashed: 'border-top: 1px dashed #e5e7eb;',\n dotted: 'border-top: 1px dotted #e5e7eb;',\n }\n \n return `<hr style=\"${styles[style] || styles.solid} margin: 24px 0; border-bottom: none; border-left: none; border-right: none;\" />`\n}\n\n/**\n * Get text alignment from format number\n */\nfunction getAlignment(format?: number): string {\n if (!format) return 'left'\n \n // Lexical alignment format values\n if (format & 2) return 'center'\n if (format & 3) return 'right'\n if (format & 4) return 'justify'\n \n return 'left'\n}\n\n/**\n * Escape HTML special characters\n */\nfunction escapeHtml(text: string): string {\n const map: Record<string, string> = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": '''\n }\n \n return text.replace(/[&<>\"']/g, m => map[m])\n}\n\n/**\n * Wrap content in a responsive email template\n */\nfunction wrapInEmailTemplate(content: string, preheader?: string): string {\n return `<!DOCTYPE html>\n<html lang=\"en\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n <meta name=\"x-apple-disable-message-reformatting\">\n <title>Newsletter</title>\n \n <!--[if mso]>\n <noscript>\n <xml>\n <o:OfficeDocumentSettings>\n <o:PixelsPerInch>96</o:PixelsPerInch>\n </o:OfficeDocumentSettings>\n </xml>\n </noscript>\n <![endif]-->\n \n <style>\n /* Reset and base styles */\n * {\n -webkit-text-size-adjust: 100%;\n -ms-text-size-adjust: 100%;\n }\n \n body {\n margin: 0 !important;\n padding: 0 !important;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;\n font-size: 16px;\n line-height: 1.5;\n color: #1A1A1A;\n background-color: #f8f9fa;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n }\n \n table {\n border-spacing: 0 !important;\n border-collapse: collapse !important;\n table-layout: fixed !important;\n margin: 0 auto !important;\n }\n \n table table table {\n table-layout: auto;\n }\n \n img {\n -ms-interpolation-mode: bicubic;\n max-width: 100%;\n height: auto;\n border: 0;\n outline: none;\n text-decoration: none;\n }\n \n /* Responsive styles */\n @media only screen and (max-width: 640px) {\n .mobile-hide {\n display: none !important;\n }\n \n .mobile-center {\n text-align: center !important;\n }\n \n .mobile-width-100 {\n width: 100% !important;\n max-width: 100% !important;\n }\n \n .mobile-padding {\n padding: 20px !important;\n }\n \n .mobile-padding-sm {\n padding: 16px !important;\n }\n \n .mobile-font-size-14 {\n font-size: 14px !important;\n }\n \n .mobile-font-size-16 {\n font-size: 16px !important;\n }\n \n .mobile-font-size-20 {\n font-size: 20px !important;\n line-height: 1.3 !important;\n }\n \n .mobile-font-size-24 {\n font-size: 24px !important;\n line-height: 1.2 !important;\n }\n \n /* Stack sections on mobile */\n .mobile-stack {\n display: block !important;\n width: 100% !important;\n }\n \n /* Mobile-specific spacing */\n .mobile-margin-bottom-16 {\n margin-bottom: 16px !important;\n }\n \n .mobile-margin-bottom-20 {\n margin-bottom: 20px !important;\n }\n }\n \n /* Dark mode support */\n @media (prefers-color-scheme: dark) {\n .dark-mode-bg {\n background-color: #1a1a1a !important;\n }\n \n .dark-mode-text {\n color: #ffffff !important;\n }\n \n .dark-mode-border {\n border-color: #333333 !important;\n }\n }\n \n /* Outlook-specific fixes */\n <!--[if mso]>\n <style>\n table {\n border-collapse: collapse;\n border-spacing: 0;\n border: none;\n margin: 0;\n }\n \n div, p {\n margin: 0;\n }\n </style>\n <![endif]-->\n </style>\n</head>\n<body style=\"margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.5; color: #1A1A1A; background-color: #f8f9fa;\">\n ${preheader ? `\n <!-- Preheader text -->\n <div style=\"display: none; max-height: 0; overflow: hidden; font-size: 1px; line-height: 1px; color: transparent;\">\n ${escapeHtml(preheader)}\n </div>\n ` : ''}\n \n <!-- Main container -->\n <table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\" style=\"margin: 0; padding: 0; background-color: #f8f9fa;\">\n <tr>\n <td align=\"center\" style=\"padding: 20px 10px;\">\n <!-- Email wrapper -->\n <table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" width=\"600\" class=\"mobile-width-100\" style=\"margin: 0 auto; max-width: 600px;\">\n <tr>\n <td class=\"mobile-padding\" style=\"padding: 0;\">\n <!-- Content area with light background -->\n <div style=\"background-color: #ffffff; padding: 40px 30px; border-radius: 8px;\" class=\"mobile-padding\">\n ${content}\n </div>\n </td>\n </tr>\n </table>\n </td>\n </tr>\n </table>\n</body>\n</html>`\n}\n\n/**\n * Extract personalization tags from content\n */\nexport function extractPersonalizationTags(html: string): string[] {\n const regex = /\\{\\{([^}]+)\\}\\}/g\n const tags: string[] = []\n let match\n \n while ((match = regex.exec(html)) !== null) {\n tags.push(match[1].trim())\n }\n \n return [...new Set(tags)]\n}\n\n/**\n * Replace personalization tags with sample data\n */\nexport function replacePersonalizationTags(\n html: string, \n sampleData: Record<string, string>\n): string {\n return html.replace(/\\{\\{([^}]+)\\}\\}/g, (match, tag) => {\n const trimmedTag = tag.trim()\n return sampleData[trimmedTag] || match\n })\n}","/**\n * Email HTML validation utilities\n */\n\nexport interface ValidationResult {\n valid: boolean\n warnings: string[]\n errors: string[]\n stats: {\n sizeInBytes: number\n imageCount: number\n linkCount: number\n hasExternalStyles: boolean\n hasJavaScript: boolean\n }\n}\n\n/**\n * Validate HTML for email compatibility\n */\nexport function validateEmailHtml(html: string): ValidationResult {\n const warnings: string[] = []\n const errors: string[] = []\n \n // Calculate size\n const sizeInBytes = new Blob([html]).size\n \n // Check size limits\n if (sizeInBytes > 102400) { // 100KB\n warnings.push(`Email size (${Math.round(sizeInBytes / 1024)}KB) exceeds Gmail's 102KB limit - email may be clipped`)\n }\n \n // Check for problematic CSS\n if (html.includes('position:') && (html.includes('position: absolute') || html.includes('position: fixed'))) {\n errors.push('Absolute/fixed positioning is not supported in most email clients')\n }\n \n if (html.includes('display: flex') || html.includes('display: grid')) {\n errors.push('Flexbox and Grid layouts are not supported in many email clients')\n }\n \n if (html.includes('@media')) {\n warnings.push('Media queries may not work in all email clients')\n }\n \n // Check for JavaScript\n const hasJavaScript = \n html.includes('<script') || \n html.includes('onclick') || \n html.includes('onload') ||\n html.includes('javascript:')\n \n if (hasJavaScript) {\n errors.push('JavaScript is not supported in email and will be stripped by email clients')\n }\n \n // Check for external styles\n const hasExternalStyles = html.includes('<link') && html.includes('stylesheet')\n if (hasExternalStyles) {\n errors.push('External stylesheets are not supported - use inline styles only')\n }\n \n // Check for forms\n if (html.includes('<form') || html.includes('<input') || html.includes('<button')) {\n errors.push('Forms and form elements are not reliably supported in email')\n }\n \n // Check for unsupported tags\n const unsupportedTags = [\n 'video', 'audio', 'iframe', 'embed', 'object', 'canvas', 'svg'\n ]\n \n for (const tag of unsupportedTags) {\n if (html.includes(`<${tag}`)) {\n errors.push(`<${tag}> tags are not supported in email`)\n }\n }\n \n // Count images and links\n const imageCount = (html.match(/<img/g) || []).length\n const linkCount = (html.match(/<a/g) || []).length\n \n // Check image usage\n if (imageCount > 20) {\n warnings.push(`High number of images (${imageCount}) may affect email performance`)\n }\n \n // Check for missing alt text\n const imagesWithoutAlt = (html.match(/<img(?![^>]*\\balt\\s*=)[^>]*>/g) || []).length\n if (imagesWithoutAlt > 0) {\n warnings.push(`${imagesWithoutAlt} image(s) missing alt text - important for accessibility`)\n }\n \n // Check for proper link attributes\n const linksWithoutTarget = (html.match(/<a(?![^>]*\\btarget\\s*=)[^>]*>/g) || []).length\n if (linksWithoutTarget > 0) {\n warnings.push(`${linksWithoutTarget} link(s) missing target=\"_blank\" attribute`)\n }\n \n // Check for CSS property usage\n if (html.includes('margin: auto') || html.includes('margin:auto')) {\n warnings.push('margin: auto is not supported in Outlook - use align=\"center\" or tables for centering')\n }\n \n if (html.includes('background-image')) {\n warnings.push('Background images are not reliably supported - consider using <img> tags instead')\n }\n \n // Check for rem/em units\n if (html.match(/\\d+\\s*(rem|em)/)) {\n warnings.push('rem/em units may render inconsistently - use px for reliable sizing')\n }\n \n // Check for negative margins\n if (html.match(/margin[^:]*:\\s*-\\d+/)) {\n errors.push('Negative margins are not supported in many email clients')\n }\n \n // Validate personalization tags\n const personalizationTags = html.match(/\\{\\{([^}]+)\\}\\}/g) || []\n const validTags = ['subscriber.name', 'subscriber.email', 'subscriber.firstName', 'subscriber.lastName']\n \n for (const tag of personalizationTags) {\n const tagContent = tag.replace(/[{}]/g, '').trim()\n if (!validTags.includes(tagContent)) {\n warnings.push(`Unknown personalization tag: ${tag}`)\n }\n }\n \n return {\n valid: errors.length === 0,\n warnings,\n errors,\n stats: {\n sizeInBytes,\n imageCount,\n linkCount,\n hasExternalStyles,\n hasJavaScript,\n }\n }\n}\n\n/**\n * Get email client compatibility warnings for specific HTML\n */\nexport function getClientCompatibilityWarnings(html: string): Record<string, string[]> {\n const warnings: Record<string, string[]> = {\n gmail: [],\n outlook: [],\n appleMail: [],\n mobile: [],\n }\n \n // Gmail specific\n if (html.includes('<style')) {\n warnings.gmail.push('Gmail may strip <style> tags in some contexts')\n }\n \n // Outlook specific\n if (html.includes('margin: auto') || html.includes('margin:auto')) {\n warnings.outlook.push('Outlook does not support margin: auto')\n }\n \n if (html.includes('padding') && html.includes('<p')) {\n warnings.outlook.push('Outlook may not respect padding on <p> tags')\n }\n \n if (html.includes('background-image')) {\n warnings.outlook.push('Outlook has limited background image support')\n }\n \n // Mobile specific\n const hasSmallText = html.match(/font-size:\\s*(\\d+)px/g)?.some(match => {\n const size = parseInt(match.match(/\\d+/)?.[0] || '16')\n return size < 14\n })\n \n if (hasSmallText) {\n warnings.mobile.push('Text smaller than 14px may be hard to read on mobile')\n }\n \n const hasSmallLinks = html.match(/<a[^>]*>[^<]{1,3}<\\/a>/g)\n if (hasSmallLinks) {\n warnings.mobile.push('Short link text may be hard to tap on mobile devices')\n }\n \n return warnings\n}\n\n/**\n * Suggest fixes for common email HTML issues\n */\nexport function suggestFixes(html: string): string[] {\n const suggestions: string[] = []\n \n if (html.includes('display: flex')) {\n suggestions.push('Replace flexbox with table-based layouts for better email client support')\n }\n \n if (html.includes('position: absolute')) {\n suggestions.push('Use table cells or margins instead of absolute positioning')\n }\n \n if (html.match(/\\d+rem/) || html.match(/\\d+em/)) {\n suggestions.push('Convert rem/em units to px for consistent rendering')\n }\n \n if (!html.includes('<!DOCTYPE')) {\n suggestions.push('Add <!DOCTYPE html> declaration for better rendering')\n }\n \n if (!html.includes('charset')) {\n suggestions.push('Add <meta charset=\"UTF-8\"> for proper character encoding')\n }\n \n return suggestions\n}","import type { PayloadRequest } from 'payload'\nimport type { NewsletterPluginConfig, BroadcastProviderConfig } from '../types'\n\nexport async function getBroadcastConfig(\n req: PayloadRequest,\n pluginConfig: NewsletterPluginConfig\n): Promise<BroadcastProviderConfig | null> {\n try {\n // Get settings from Newsletter Settings collection\n const settings = await req.payload.findGlobal({\n slug: pluginConfig.settingsSlug || 'newsletter-settings',\n req,\n })\n\n // Build provider config from settings, falling back to env vars\n if (settings?.provider === 'broadcast' && settings?.broadcastSettings) {\n return {\n apiUrl: settings.broadcastSettings.apiUrl || pluginConfig.providers?.broadcast?.apiUrl || '',\n token: settings.broadcastSettings.token || pluginConfig.providers?.broadcast?.token || '',\n fromAddress: settings.fromAddress || pluginConfig.providers?.broadcast?.fromAddress || '',\n fromName: settings.fromName || pluginConfig.providers?.broadcast?.fromName || '',\n replyTo: settings.replyTo || pluginConfig.providers?.broadcast?.replyTo,\n }\n }\n\n // Fall back to env var config\n return pluginConfig.providers?.broadcast || null\n } catch (error) {\n req.payload.logger.error('Failed to get broadcast config from settings:', error)\n // Fall back to env var config on error\n return pluginConfig.providers?.broadcast || null\n }\n}","import type { PayloadRequest } from 'payload'\nimport type { NewsletterPluginConfig, ResendProviderConfig } from '../types'\n\nexport async function getResendConfig(\n req: PayloadRequest,\n pluginConfig: NewsletterPluginConfig\n): Promise<ResendProviderConfig | null> {\n try {\n // Get settings from Newsletter Settings collection\n const settings = await req.payload.findGlobal({\n slug: pluginConfig.settingsSlug || 'newsletter-settings',\n req,\n })\n\n // Build provider config from settings, falling back to env vars\n if (settings?.provider === 'resend' && settings?.resendSettings) {\n return {\n apiKey: settings.resendSettings.apiKey || pluginConfig.providers?.resend?.apiKey || '',\n fromAddress: settings.fromAddress || pluginConfig.providers?.resend?.fromAddress || '',\n fromName: settings.fromName || pluginConfig.providers?.resend?.fromName || '',\n audienceIds: settings.resendSettings.audienceIds || pluginConfig.providers?.resend?.audienceIds,\n }\n }\n\n // Fall back to env var config\n return pluginConfig.providers?.resend || null\n } catch (error) {\n req.payload.logger.error('Failed to get resend config from settings:', error)\n // Fall back to env var config on error\n return pluginConfig.providers?.resend || null\n }\n}"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,kCAAsB;AAMf,IAAM,oBAAoB;AAAA,EAC/B,cAAc;AAAA,IACZ;AAAA,IAAK;AAAA,IAAM;AAAA,IAAU;AAAA,IAAK;AAAA,IAAM;AAAA,IAAK;AAAA,IAAK;AAAA,IAAU;AAAA,IAAK;AAAA,IACzD;AAAA,IAAK;AAAA,IAAM;AAAA,IAAM;AAAA,IAAM;AAAA,IAAM;AAAA,IAAM;AAAA,IAAM;AAAA,IAAc;AAAA,IACvD;AAAA,IAAO;AAAA,IAAO;AAAA,IAAS;AAAA,IAAM;AAAA,IAAM;AAAA,IAAM;AAAA,IAAS;AAAA,EACpD;AAAA,EACA,cAAc,CAAC,QAAQ,SAAS,UAAU,OAAO,SAAS,OAAO,OAAO,SAAS,UAAU,UAAU,eAAe,aAAa;AAAA,EACjI,gBAAgB;AAAA,IACd,KAAK;AAAA,MACH;AAAA,MAAS;AAAA,MAAoB;AAAA,MAAa;AAAA,MAC1C;AAAA,MAAc;AAAA,MAAmB;AAAA,MAAc;AAAA,MAC/C;AAAA,MAAc;AAAA,MAAgB;AAAA,MAAiB;AAAA,MAC/C;AAAA,MAAW;AAAA,MAAe;AAAA,MAAiB;AAAA,MAC3C;AAAA,MAAgB;AAAA,MAAe;AAAA,MAAe;AAAA,MAC9C;AAAA,MAAqB;AAAA,IACvB;AAAA,EACF;AAAA,EACA,aAAa,CAAC,UAAU,SAAS,UAAU,UAAU,SAAS,QAAQ,OAAO;AAAA,EAC7E,aAAa,CAAC,SAAS,MAAM,WAAW,UAAU,SAAS;AAC7D;AAKA,eAAsB,uBACpB,aACA,SAWiB;AAEjB,MAAI,CAAC,aAAa;AAChB,WAAO;AAAA,EACT;AAGA,QAAM,UAAU,MAAM,mBAAmB,aAAa,SAAS,UAAU,SAAS,oBAAoB;AAGtG,QAAM,gBAAgB,4BAAAA,QAAU,SAAS,SAAS,iBAAiB;AAGnE,MAAI,SAAS,gBAAgB;AAC3B,QAAI,QAAQ,eAAe;AACzB,aAAO,MAAM,QAAQ,QAAQ,QAAQ,cAAc,eAAe;AAAA,QAChE,WAAW,QAAQ;AAAA,QACnB,SAAS,QAAQ;AAAA,QACjB,cAAc,QAAQ;AAAA,MACxB,CAAC,CAAC;AAAA,IACJ;AACA,WAAO,oBAAoB,eAAe,QAAQ,SAAS;AAAA,EAC7D;AAEA,SAAO;AACT;AAKA,eAAe,mBACb,aACA,UACA,sBACiB;AACjB,QAAM,EAAE,KAAK,IAAI;AAEjB,MAAI,CAAC,QAAQ,CAAC,KAAK,UAAU;AAC3B,WAAO;AAAA,EACT;AAGA,QAAM,YAAY,MAAM,QAAQ;AAAA,IAC9B,KAAK,SAAS,IAAI,CAAC,SAAc,YAAY,MAAM,UAAU,oBAAoB,CAAC;AAAA,EACpF;AAEA,SAAO,UAAU,KAAK,EAAE;AAC1B;AAMA,eAAe,YACb,MACA,UACA,sBACiB;AACjB,UAAQ,KAAK,MAAM;AAAA,IACjB,KAAK;AACH,aAAO,iBAAiB,MAAM,UAAU,oBAAoB;AAAA,IAC9D,KAAK;AACH,aAAO,eAAe,MAAM,UAAU,oBAAoB;AAAA,IAC5D,KAAK;AACH,aAAO,YAAY,MAAM,UAAU,oBAAoB;AAAA,IACzD,KAAK;AACH,aAAO,gBAAgB,MAAM,UAAU,oBAAoB;AAAA,IAC7D,KAAK;AACH,aAAO,kBAAkB,MAAM,UAAU,oBAAoB;AAAA,IAC/D,KAAK;AACH,aAAO,YAAY,IAAI;AAAA,IACzB,KAAK;AACH,aAAO,YAAY,MAAM,UAAU,oBAAoB;AAAA,IACzD,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO,cAAc,MAAM,QAAQ;AAAA,IACrC,KAAK;AACH,aAAO,MAAM,aAAa,MAAM,UAAU,oBAAoB;AAAA,IAChE;AAEE,UAAI,KAAK,UAAU;AACjB,cAAM,aAAa,MAAM,QAAQ;AAAA,UAC/B,KAAK,SAAS,IAAI,CAAC,UAAe,YAAY,OAAO,UAAU,oBAAoB,CAAC;AAAA,QACtF;AACA,eAAO,WAAW,KAAK,EAAE;AAAA,MAC3B;AACA,aAAO;AAAA,EACX;AACF;AAMA,eAAe,iBACb,MACA,UACA,sBACiB;AACjB,QAAM,QAAQ,aAAa,KAAK,MAAM;AACtC,QAAM,aAAa,MAAM,QAAQ;AAAA,KAC9B,KAAK,YAAY,CAAC,GAAG,IAAI,CAAC,UAAe,YAAY,OAAO,UAAU,oBAAoB,CAAC;AAAA,EAC9F;AACA,QAAM,WAAW,WAAW,KAAK,EAAE;AAEnC,MAAI,CAAC,SAAS,KAAK,GAAG;AACpB,WAAO;AAAA,EACT;AAEA,SAAO,6EAA6E,KAAK,yCAAyC,QAAQ;AAC5I;AAMA,eAAe,eACb,MACA,UACA,sBACiB;AACjB,QAAM,MAAM,KAAK,OAAO;AACxB,QAAM,QAAQ,aAAa,KAAK,MAAM;AACtC,QAAM,aAAa,MAAM,QAAQ;AAAA,KAC9B,KAAK,YAAY,CAAC,GAAG,IAAI,CAAC,UAAe,YAAY,OAAO,UAAU,oBAAoB,CAAC;AAAA,EAC9F;AACA,QAAM,WAAW,WAAW,KAAK,EAAE;AAEnC,QAAM,SAAiC;AAAA,IACrC,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,EACN;AAEA,QAAM,gBAAwC;AAAA,IAC5C,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,EACN;AAEA,QAAM,QAAQ,GAAG,OAAO,GAAG,KAAK,OAAO,EAAE,gBAAgB,KAAK;AAC9D,QAAM,cAAc,cAAc,GAAG,KAAK,cAAc;AAExD,SAAO,IAAI,GAAG,WAAW,WAAW,YAAY,KAAK,KAAK,QAAQ,KAAK,GAAG;AAC5E;AAMA,eAAe,YACb,MACA,UACA,sBACiB;AACjB,QAAM,MAAM,KAAK,aAAa,WAAW,OAAO;AAChD,QAAM,aAAa,MAAM,QAAQ;AAAA,KAC9B,KAAK,YAAY,CAAC,GAAG,IAAI,CAAC,UAAe,YAAY,OAAO,UAAU,oBAAoB,CAAC;AAAA,EAC9F;AACA,QAAM,WAAW,WAAW,KAAK,EAAE;AAEnC,QAAM,QAAQ,QAAQ,OAClB,sGACA;AAEJ,SAAO,IAAI,GAAG,2CAA2C,KAAK,KAAK,QAAQ,KAAK,GAAG;AACrF;AAMA,eAAe,gBACb,MACA,UACA,sBACiB;AACjB,QAAM,aAAa,MAAM,QAAQ;AAAA,KAC9B,KAAK,YAAY,CAAC,GAAG,IAAI,CAAC,UAAe,YAAY,OAAO,UAAU,oBAAoB,CAAC;AAAA,EAC9F;AACA,QAAM,WAAW,WAAW,KAAK,EAAE;AACnC,SAAO,kCAAkC,QAAQ;AACnD;AAMA,eAAe,kBACb,MACA,UACA,sBACiB;AACjB,QAAM,aAAa,MAAM,QAAQ;AAAA,KAC9B,KAAK,YAAY,CAAC,GAAG,IAAI,CAAC,UAAe,YAAY,OAAO,UAAU,oBAAoB,CAAC;AAAA,EAC9F;AACA,QAAM,WAAW,WAAW,KAAK,EAAE;AACnC,QAAM,QAAQ;AAEd,SAAO,sBAAsB,KAAK,KAAK,QAAQ;AACjD;AAMA,SAAS,YAAY,MAAmB;AACtC,MAAI,OAAO,WAAW,KAAK,QAAQ,EAAE;AAGrC,MAAI,KAAK,SAAS,GAAG;AACnB,WAAO,WAAW,IAAI;AAAA,EACxB;AACA,MAAI,KAAK,SAAS,GAAG;AACnB,WAAO,OAAO,IAAI;AAAA,EACpB;AACA,MAAI,KAAK,SAAS,GAAG;AACnB,WAAO,MAAM,IAAI;AAAA,EACnB;AACA,MAAI,KAAK,SAAS,GAAG;AACnB,WAAO,WAAW,IAAI;AAAA,EACxB;AAEA,SAAO;AACT;AAMA,eAAe,YACb,MACA,UACA,sBACiB;AACjB,QAAM,aAAa,MAAM,QAAQ;AAAA,KAC9B,KAAK,YAAY,CAAC,GAAG,IAAI,CAAC,UAAe,YAAY,OAAO,UAAU,oBAAoB,CAAC;AAAA,EAC9F;AACA,QAAM,WAAW,WAAW,KAAK,EAAE;AACnC,QAAM,MAAM,KAAK,QAAQ,OAAO;AAChC,QAAM,SAAS,KAAK,QAAQ,UAAU;AAGtC,QAAM,aAAa,SAAS,qBAAqB;AACjD,QAAM,UAAU,SAAS,+BAA+B;AAExD,SAAO,YAAY,WAAW,GAAG,CAAC,IAAI,UAAU,GAAG,OAAO,wDAAwD,QAAQ;AAC5H;AAMA,SAAS,cAAc,MAAW,UAA2B;AAC3D,QAAM,SAAS,KAAK;AACpB,MAAI,CAAC,OAAQ,QAAO;AAGpB,MAAI,MAAM;AACV,MAAI,OAAO,WAAW,UAAU;AAC9B,UAAM;AAAA,EACR,WAAW,OAAO,KAAK;AACrB,UAAM,OAAO;AAAA,EACf,WAAW,OAAO,YAAY,UAAU;AAEtC,UAAM,GAAG,QAAQ,IAAI,OAAO,QAAQ;AAAA,EACtC;AAEA,QAAM,MAAM,KAAK,QAAQ,WAAW,OAAO,OAAO;AAClD,QAAM,UAAU,KAAK,QAAQ,WAAW;AAGxC,QAAM,UAAU,aAAa,WAAW,GAAG,CAAC,UAAU,WAAW,GAAG,CAAC;AAErE,MAAI,SAAS;AACX,WAAO;AAAA;AAAA,UAED,OAAO;AAAA,6IAC4H,WAAW,OAAO,CAAC;AAAA;AAAA;AAAA,EAG9J;AAEA,SAAO,wFAAwF,OAAO;AACxG;AAMA,eAAe,aACb,MACA,UACA,sBACiB;AACjB,QAAM,YAAY,KAAK,QAAQ,aAAa,KAAK;AAGjD,MAAI,sBAAsB;AACxB,QAAI;AACF,YAAM,aAAa,MAAM,qBAAqB,MAAM,QAAQ;AAC5D,UAAI,YAAY;AACd,eAAO;AAAA,MACT;AAAA,IACF,SAAS,OAAO;AACd,cAAQ,MAAM,oCAAoC,SAAS,KAAK,KAAK;AAAA,IAEvE;AAAA,EACF;AAGA,UAAQ,WAAW;AAAA,IACjB,KAAK;AACH,aAAO,mBAAmB,KAAK,MAAM;AAAA,IACvC,KAAK;AACH,aAAO,oBAAoB,KAAK,MAAM;AAAA,IACxC;AAEE,UAAI,KAAK,UAAU;AACjB,cAAM,aAAa,MAAM,QAAQ;AAAA,UAC/B,KAAK,SAAS,IAAI,CAAC,UAAe,YAAY,OAAO,UAAU,oBAAoB,CAAC;AAAA,QACtF;AACA,eAAO,WAAW,KAAK,EAAE;AAAA,MAC3B;AACA,aAAO;AAAA,EACX;AACF;AAMA,SAAS,mBAAmB,QAAqB;AAC/C,QAAM,OAAO,QAAQ,QAAQ;AAC7B,QAAM,MAAM,QAAQ,OAAO;AAC3B,QAAM,QAAQ,QAAQ,SAAS;AAE/B,QAAM,SAAiC;AAAA,IACrC,SAAS;AAAA,IACT,WAAW;AAAA,IACX,SAAS;AAAA,EACX;AAEA,QAAM,cAAc,GAAG,OAAO,KAAK,KAAK,OAAO,OAAO;AAEtD,SAAO;AAAA;AAAA,iBAEQ,WAAW,GAAG,CAAC,sDAAsD,WAAW,KAAK,WAAW,IAAI,CAAC;AAAA;AAAA;AAGtH;AAMA,SAAS,oBAAoB,QAAqB;AAChD,QAAM,QAAQ,QAAQ,SAAS;AAE/B,QAAM,SAAiC;AAAA,IACrC,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,QAAQ;AAAA,EACV;AAEA,SAAO,cAAc,OAAO,KAAK,KAAK,OAAO,KAAK;AACpD;AAKA,SAAS,aAAa,QAAyB;AAC7C,MAAI,CAAC,OAAQ,QAAO;AAGpB,MAAI,SAAS,EAAG,QAAO;AACvB,MAAI,SAAS,EAAG,QAAO;AACvB,MAAI,SAAS,EAAG,QAAO;AAEvB,SAAO;AACT;AAKA,SAAS,WAAW,MAAsB;AACxC,QAAM,MAA8B;AAAA,IAClC,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,EACP;AAEA,SAAO,KAAK,QAAQ,YAAY,OAAK,IAAI,CAAC,CAAC;AAC7C;AAKA,SAAS,oBAAoB,SAAiB,WAA4B;AACxE,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAoJL,YAAY;AAAA;AAAA;AAAA,MAGV,WAAW,SAAS,CAAC;AAAA;AAAA,MAErB,EAAE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,kBAYU,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUzB;;;ACxlBO,SAAS,kBAAkB,MAAgC;AAChE,QAAM,WAAqB,CAAC;AAC5B,QAAM,SAAmB,CAAC;AAG1B,QAAM,cAAc,IAAI,KAAK,CAAC,IAAI,CAAC,EAAE;AAGrC,MAAI,cAAc,QAAQ;AACxB,aAAS,KAAK,eAAe,KAAK,MAAM,cAAc,IAAI,CAAC,wDAAwD;AAAA,EACrH;AAGA,MAAI,KAAK,SAAS,WAAW,MAAM,KAAK,SAAS,oBAAoB,KAAK,KAAK,SAAS,iBAAiB,IAAI;AAC3G,WAAO,KAAK,mEAAmE;AAAA,EACjF;AAEA,MAAI,KAAK,SAAS,eAAe,KAAK,KAAK,SAAS,eAAe,GAAG;AACpE,WAAO,KAAK,kEAAkE;AAAA,EAChF;AAEA,MAAI,KAAK,SAAS,QAAQ,GAAG;AAC3B,aAAS,KAAK,iDAAiD;AAAA,EACjE;AAGA,QAAM,gBACJ,KAAK,SAAS,SAAS,KACvB,KAAK,SAAS,SAAS,KACvB,KAAK,SAAS,QAAQ,KACtB,KAAK,SAAS,aAAa;AAE7B,MAAI,eAAe;AACjB,WAAO,KAAK,4EAA4E;AAAA,EAC1F;AAGA,QAAM,oBAAoB,KAAK,SAAS,OAAO,KAAK,KAAK,SAAS,YAAY;AAC9E,MAAI,mBAAmB;AACrB,WAAO,KAAK,iEAAiE;AAAA,EAC/E;AAGA,MAAI,KAAK,SAAS,OAAO,KAAK,KAAK,SAAS,QAAQ,KAAK,KAAK,SAAS,SAAS,GAAG;AACjF,WAAO,KAAK,6DAA6D;AAAA,EAC3E;AAGA,QAAM,kBAAkB;AAAA,IACtB;AAAA,IAAS;AAAA,IAAS;AAAA,IAAU;AAAA,IAAS;AAAA,IAAU;AAAA,IAAU;AAAA,EAC3D;AAEA,aAAW,OAAO,iBAAiB;AACjC,QAAI,KAAK,SAAS,IAAI,GAAG,EAAE,GAAG;AAC5B,aAAO,KAAK,IAAI,GAAG,mCAAmC;AAAA,IACxD;AAAA,EACF;AAGA,QAAM,cAAc,KAAK,MAAM,OAAO,KAAK,CAAC,GAAG;AAC/C,QAAM,aAAa,KAAK,MAAM,KAAK,KAAK,CAAC,GAAG;AAG5C,MAAI,aAAa,IAAI;AACnB,aAAS,KAAK,0BAA0B,UAAU,gCAAgC;AAAA,EACpF;AAGA,QAAM,oBAAoB,KAAK,MAAM,+BAA+B,KAAK,CAAC,GAAG;AAC7E,MAAI,mBAAmB,GAAG;AACxB,aAAS,KAAK,GAAG,gBAAgB,0DAA0D;AAAA,EAC7F;AAGA,QAAM,sBAAsB,KAAK,MAAM,gCAAgC,KAAK,CAAC,GAAG;AAChF,MAAI,qBAAqB,GAAG;AAC1B,aAAS,KAAK,GAAG,kBAAkB,4CAA4C;AAAA,EACjF;AAGA,MAAI,KAAK,SAAS,cAAc,KAAK,KAAK,SAAS,aAAa,GAAG;AACjE,aAAS,KAAK,uFAAuF;AAAA,EACvG;AAEA,MAAI,KAAK,SAAS,kBAAkB,GAAG;AACrC,aAAS,KAAK,kFAAkF;AAAA,EAClG;AAGA,MAAI,KAAK,MAAM,gBAAgB,GAAG;AAChC,aAAS,KAAK,qEAAqE;AAAA,EACrF;AAGA,MAAI,KAAK,MAAM,qBAAqB,GAAG;AACrC,WAAO,KAAK,0DAA0D;AAAA,EACxE;AAGA,QAAM,sBAAsB,KAAK,MAAM,kBAAkB,KAAK,CAAC;AAC/D,QAAM,YAAY,CAAC,mBAAmB,oBAAoB,wBAAwB,qBAAqB;AAEvG,aAAW,OAAO,qBAAqB;AACrC,UAAM,aAAa,IAAI,QAAQ,SAAS,EAAE,EAAE,KAAK;AACjD,QAAI,CAAC,UAAU,SAAS,UAAU,GAAG;AACnC,eAAS,KAAK,gCAAgC,GAAG,EAAE;AAAA,IACrD;AAAA,EACF;AAEA,SAAO;AAAA,IACL,OAAO,OAAO,WAAW;AAAA,IACzB;AAAA,IACA;AAAA,IACA,OAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;;;AC1IA,eAAsB,mBACpB,KACA,cACyC;AACzC,MAAI;AAEF,UAAM,WAAW,MAAM,IAAI,QAAQ,WAAW;AAAA,MAC5C,MAAM,aAAa,gBAAgB;AAAA,MACnC;AAAA,IACF,CAAC;AAGD,QAAI,UAAU,aAAa,eAAe,UAAU,mBAAmB;AACrE,aAAO;AAAA,QACL,QAAQ,SAAS,kBAAkB,UAAU,aAAa,WAAW,WAAW,UAAU;AAAA,QAC1F,OAAO,SAAS,kBAAkB,SAAS,aAAa,WAAW,WAAW,SAAS;AAAA,QACvF,aAAa,SAAS,eAAe,aAAa,WAAW,WAAW,eAAe;AAAA,QACvF,UAAU,SAAS,YAAY,aAAa,WAAW,WAAW,YAAY;AAAA,QAC9E,SAAS,SAAS,WAAW,aAAa,WAAW,WAAW;AAAA,MAClE;AAAA,IACF;AAGA,WAAO,aAAa,WAAW,aAAa;AAAA,EAC9C,SAAS,OAAO;AACd,QAAI,QAAQ,OAAO,MAAM,iDAAiD,KAAK;AAE/E,WAAO,aAAa,WAAW,aAAa;AAAA,EAC9C;AACF;;;AC7BA,eAAsB,gBACpB,KACA,cACsC;AACtC,MAAI;AAEF,UAAM,WAAW,MAAM,IAAI,QAAQ,WAAW;AAAA,MAC5C,MAAM,aAAa,gBAAgB;AAAA,MACnC;AAAA,IACF,CAAC;AAGD,QAAI,UAAU,aAAa,YAAY,UAAU,gBAAgB;AAC/D,aAAO;AAAA,QACL,QAAQ,SAAS,eAAe,UAAU,aAAa,WAAW,QAAQ,UAAU;AAAA,QACpF,aAAa,SAAS,eAAe,aAAa,WAAW,QAAQ,eAAe;AAAA,QACpF,UAAU,SAAS,YAAY,aAAa,WAAW,QAAQ,YAAY;AAAA,QAC3E,aAAa,SAAS,eAAe,eAAe,aAAa,WAAW,QAAQ;AAAA,MACtF;AAAA,IACF;AAGA,WAAO,aAAa,WAAW,UAAU;AAAA,EAC3C,SAAS,OAAO;AACd,QAAI,QAAQ,OAAO,MAAM,8CAA8C,KAAK;AAE5E,WAAO,aAAa,WAAW,UAAU;AAAA,EAC3C;AACF;","names":["DOMPurify"]}
|
|
1
|
+
{"version":3,"sources":["../src/exports/utils.ts","../src/utils/emailSafeHtml.ts","../src/utils/validateEmailHtml.ts","../src/utils/getBroadcastConfig.ts","../src/utils/getResendConfig.ts"],"sourcesContent":["// Email utilities\nexport { convertToEmailSafeHtml, EMAIL_SAFE_CONFIG } from '../utils/emailSafeHtml'\nexport { validateEmailHtml } from '../utils/validateEmailHtml'\nexport type { ValidationResult } from '../utils/validateEmailHtml'\n\n// Configuration utilities\nexport { getBroadcastConfig } from '../utils/getBroadcastConfig'\nexport { getResendConfig } from '../utils/getResendConfig'","import DOMPurify from 'isomorphic-dompurify'\nimport type { SerializedEditorState } from 'lexical'\n\n/**\n * DOMPurify configuration for email-safe HTML\n */\nexport const EMAIL_SAFE_CONFIG = {\n ALLOWED_TAGS: [\n 'p', 'br', 'strong', 'b', 'em', 'i', 'u', 'strike', 's', 'span',\n 'a', 'h1', 'h2', 'h3', 'ul', 'ol', 'li', 'blockquote', 'hr',\n 'img', 'div', 'table', 'tr', 'td', 'th', 'tbody', 'thead'\n ],\n ALLOWED_ATTR: ['href', 'style', 'target', 'rel', 'align', 'src', 'alt', 'width', 'height', 'border', 'cellpadding', 'cellspacing'],\n ALLOWED_STYLES: {\n '*': [\n 'color', 'background-color', 'font-size', 'font-weight',\n 'font-style', 'text-decoration', 'text-align', 'margin',\n 'margin-top', 'margin-right', 'margin-bottom', 'margin-left',\n 'padding', 'padding-top', 'padding-right', 'padding-bottom', \n 'padding-left', 'line-height', 'border-left', 'border-left-width',\n 'border-left-style', 'border-left-color'\n ],\n },\n FORBID_TAGS: ['script', 'style', 'iframe', 'object', 'embed', 'form', 'input'],\n FORBID_ATTR: ['class', 'id', 'onclick', 'onload', 'onerror'],\n}\n\n/**\n * Converts Lexical editor state to email-safe HTML\n */\nexport async function convertToEmailSafeHtml(\n editorState: SerializedEditorState | undefined | null,\n options?: {\n wrapInTemplate?: boolean\n preheader?: string\n mediaUrl?: string // Base URL for media files\n customBlockConverter?: (node: any, mediaUrl?: string) => Promise<string>\n payload?: any // Payload instance for populating relationships\n populateFields?: string[] | ((blockType: string) => string[]) // Fields to populate\n customWrapper?: (content: string, options?: { preheader?: string; subject?: string; documentData?: Record<string, any> }) => string | Promise<string>\n subject?: string // Email subject for custom wrapper\n documentData?: Record<string, any> // Generic document data for custom wrapper\n }\n): Promise<string> {\n // Handle empty content\n if (!editorState) {\n return ''\n }\n \n // First, convert Lexical state to HTML using custom converters\n const rawHtml = await lexicalToEmailHtml(editorState, options?.mediaUrl, options?.customBlockConverter)\n \n // Sanitize the HTML\n const sanitizedHtml = DOMPurify.sanitize(rawHtml, EMAIL_SAFE_CONFIG)\n \n // Optionally wrap in email template\n if (options?.wrapInTemplate) {\n if (options.customWrapper) {\n return await Promise.resolve(options.customWrapper(sanitizedHtml, { \n preheader: options.preheader,\n subject: options.subject,\n documentData: options.documentData\n }))\n }\n return wrapInEmailTemplate(sanitizedHtml, options.preheader)\n }\n \n return sanitizedHtml\n}\n\n/**\n * Custom Lexical to HTML converter for email\n */\nasync function lexicalToEmailHtml(\n editorState: SerializedEditorState, \n mediaUrl?: string,\n customBlockConverter?: (node: any, mediaUrl?: string) => Promise<string>\n): Promise<string> {\n const { root } = editorState\n \n if (!root || !root.children) {\n return ''\n }\n \n // Convert nodes asynchronously to support custom converters\n const htmlParts = await Promise.all(\n root.children.map((node: any) => convertNode(node, mediaUrl, customBlockConverter))\n )\n \n return htmlParts.join('')\n}\n\n/**\n * Convert individual Lexical nodes to email-safe HTML\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nasync function convertNode(\n node: any, \n mediaUrl?: string,\n customBlockConverter?: (node: any, mediaUrl?: string) => Promise<string>\n): Promise<string> {\n switch (node.type) {\n case 'paragraph':\n return convertParagraph(node, mediaUrl, customBlockConverter)\n case 'heading':\n return convertHeading(node, mediaUrl, customBlockConverter)\n case 'list':\n return convertList(node, mediaUrl, customBlockConverter)\n case 'listitem':\n return convertListItem(node, mediaUrl, customBlockConverter)\n case 'blockquote':\n return convertBlockquote(node, mediaUrl, customBlockConverter)\n case 'text':\n return convertText(node)\n case 'link':\n return convertLink(node, mediaUrl, customBlockConverter)\n case 'linebreak':\n return '<br>'\n case 'upload':\n return convertUpload(node, mediaUrl)\n case 'block':\n return await convertBlock(node, mediaUrl, customBlockConverter)\n default:\n // Unknown node type - convert children if any\n if (node.children) {\n const childParts = await Promise.all(\n node.children.map((child: any) => convertNode(child, mediaUrl, customBlockConverter))\n )\n return childParts.join('')\n }\n return ''\n }\n}\n\n/**\n * Convert paragraph node\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nasync function convertParagraph(\n node: any, \n mediaUrl?: string,\n customBlockConverter?: (node: any, mediaUrl?: string) => Promise<string>\n): Promise<string> {\n const align = getAlignment(node.format)\n const childParts = await Promise.all(\n (node.children || []).map((child: any) => convertNode(child, mediaUrl, customBlockConverter))\n )\n const children = childParts.join('')\n \n if (!children.trim()) {\n return '<p class=\"mobile-margin-bottom-16\" style=\"margin: 0 0 16px 0; min-height: 1em;\"> </p>'\n }\n \n return `<p class=\"mobile-margin-bottom-16\" style=\"margin: 0 0 16px 0; text-align: ${align}; font-size: 16px; line-height: 1.5;\">${children}</p>`\n}\n\n/**\n * Convert heading node\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nasync function convertHeading(\n node: any, \n mediaUrl?: string,\n customBlockConverter?: (node: any, mediaUrl?: string) => Promise<string>\n): Promise<string> {\n const tag = node.tag || 'h1'\n const align = getAlignment(node.format)\n const childParts = await Promise.all(\n (node.children || []).map((child: any) => convertNode(child, mediaUrl, customBlockConverter))\n )\n const children = childParts.join('')\n \n const styles: Record<string, string> = {\n h1: 'font-size: 32px; font-weight: 700; margin: 0 0 24px 0; line-height: 1.2;',\n h2: 'font-size: 24px; font-weight: 600; margin: 0 0 16px 0; line-height: 1.3;',\n h3: 'font-size: 20px; font-weight: 600; margin: 0 0 12px 0; line-height: 1.4;',\n }\n \n const mobileClasses: Record<string, string> = {\n h1: 'mobile-font-size-24',\n h2: 'mobile-font-size-20',\n h3: 'mobile-font-size-16',\n }\n \n const style = `${styles[tag] || styles.h3} text-align: ${align};`\n const mobileClass = mobileClasses[tag] || mobileClasses.h3\n \n return `<${tag} class=\"${mobileClass}\" style=\"${style}\">${children}</${tag}>`\n}\n\n/**\n * Convert list node\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nasync function convertList(\n node: any, \n mediaUrl?: string,\n customBlockConverter?: (node: any, mediaUrl?: string) => Promise<string>\n): Promise<string> {\n const tag = node.listType === 'number' ? 'ol' : 'ul'\n const childParts = await Promise.all(\n (node.children || []).map((child: any) => convertNode(child, mediaUrl, customBlockConverter))\n )\n const children = childParts.join('')\n \n const style = tag === 'ul' \n ? 'margin: 0 0 16px 0; padding-left: 24px; list-style-type: disc; font-size: 16px; line-height: 1.5;'\n : 'margin: 0 0 16px 0; padding-left: 24px; list-style-type: decimal; font-size: 16px; line-height: 1.5;'\n \n return `<${tag} class=\"mobile-margin-bottom-16\" style=\"${style}\">${children}</${tag}>`\n}\n\n/**\n * Convert list item node\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nasync function convertListItem(\n node: any, \n mediaUrl?: string,\n customBlockConverter?: (node: any, mediaUrl?: string) => Promise<string>\n): Promise<string> {\n const childParts = await Promise.all(\n (node.children || []).map((child: any) => convertNode(child, mediaUrl, customBlockConverter))\n )\n const children = childParts.join('')\n return `<li style=\"margin: 0 0 8px 0;\">${children}</li>`\n}\n\n/**\n * Convert blockquote node\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nasync function convertBlockquote(\n node: any, \n mediaUrl?: string,\n customBlockConverter?: (node: any, mediaUrl?: string) => Promise<string>\n): Promise<string> {\n const childParts = await Promise.all(\n (node.children || []).map((child: any) => convertNode(child, mediaUrl, customBlockConverter))\n )\n const children = childParts.join('')\n const style = 'margin: 0 0 16px 0; padding-left: 16px; border-left: 4px solid #e5e7eb; color: #6b7280;'\n \n return `<blockquote style=\"${style}\">${children}</blockquote>`\n}\n\n/**\n * Convert text node\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction convertText(node: any): string {\n let text = escapeHtml(node.text || '')\n \n // Apply formatting\n if (node.format & 1) { // Bold\n text = `<strong>${text}</strong>`\n }\n if (node.format & 2) { // Italic\n text = `<em>${text}</em>`\n }\n if (node.format & 8) { // Underline\n text = `<u>${text}</u>`\n }\n if (node.format & 4) { // Strikethrough\n text = `<strike>${text}</strike>`\n }\n \n return text\n}\n\n/**\n * Convert link node\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nasync function convertLink(\n node: any, \n mediaUrl?: string,\n customBlockConverter?: (node: any, mediaUrl?: string) => Promise<string>\n): Promise<string> {\n const childParts = await Promise.all(\n (node.children || []).map((child: any) => convertNode(child, mediaUrl, customBlockConverter))\n )\n const children = childParts.join('')\n const url = node.fields?.url || '#'\n const newTab = node.fields?.newTab ?? false\n \n // Add target and rel attributes based on newTab setting\n const targetAttr = newTab ? ' target=\"_blank\"' : ''\n const relAttr = newTab ? ' rel=\"noopener noreferrer\"' : ''\n \n return `<a href=\"${escapeHtml(url)}\"${targetAttr}${relAttr} style=\"color: #2563eb; text-decoration: underline;\">${children}</a>`\n}\n\n/**\n * Convert upload (image) node\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction convertUpload(node: any, mediaUrl?: string): string {\n const upload = node.value\n if (!upload) return ''\n \n // Get image URL - handle both direct URL and media object\n let src = ''\n if (typeof upload === 'string') {\n src = upload\n } else if (upload.url) {\n src = upload.url\n } else if (upload.filename && mediaUrl) {\n // Construct URL from media URL and filename\n src = `${mediaUrl}/${upload.filename}`\n }\n \n const alt = node.fields?.altText || upload.alt || ''\n const caption = node.fields?.caption || ''\n \n // Responsive email-safe image\n const imgHtml = `<img src=\"${escapeHtml(src)}\" alt=\"${escapeHtml(alt)}\" class=\"mobile-width-100\" style=\"max-width: 100%; height: auto; display: block; margin: 0 auto; border-radius: 6px;\" />`\n \n if (caption) {\n return `\n <div style=\"margin: 0 0 16px 0; text-align: center;\" class=\"mobile-margin-bottom-16\">\n ${imgHtml}\n <p style=\"margin: 8px 0 0 0; font-size: 14px; color: #6b7280; font-style: italic; text-align: center;\" class=\"mobile-font-size-14\">${escapeHtml(caption)}</p>\n </div>\n `\n }\n \n return `<div style=\"margin: 0 0 16px 0; text-align: center;\" class=\"mobile-margin-bottom-16\">${imgHtml}</div>`\n}\n\n/**\n * Convert custom block node\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nasync function convertBlock(\n node: any, \n mediaUrl?: string,\n customBlockConverter?: (node: any, mediaUrl?: string) => Promise<string>\n): Promise<string> {\n const blockType = node.fields?.blockName || node.blockName\n \n // First, check if there's a custom converter for this block\n if (customBlockConverter) {\n try {\n const customHtml = await customBlockConverter(node, mediaUrl)\n if (customHtml) {\n return customHtml\n }\n } catch (error) {\n console.error(`Custom block converter error for ${blockType}:`, error)\n // Fall through to default handling\n }\n }\n \n // Default handling for built-in blocks\n switch (blockType) {\n case 'button':\n return convertButtonBlock(node.fields)\n case 'divider':\n return convertDividerBlock(node.fields)\n default:\n // Unknown block type - try to convert children\n if (node.children) {\n const childParts = await Promise.all(\n node.children.map((child: any) => convertNode(child, mediaUrl, customBlockConverter))\n )\n return childParts.join('')\n }\n return ''\n }\n}\n\n/**\n * Convert button block\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction convertButtonBlock(fields: any): string {\n const text = fields?.text || 'Click here'\n const url = fields?.url || '#'\n const style = fields?.style || 'primary'\n \n const styles: Record<string, string> = {\n primary: 'background-color: #2563eb; color: #ffffff; border: 2px solid #2563eb;',\n secondary: 'background-color: #6b7280; color: #ffffff; border: 2px solid #6b7280;',\n outline: 'background-color: transparent; color: #2563eb; border: 2px solid #2563eb;',\n }\n \n const buttonStyle = `${styles[style] || styles.primary} display: inline-block; padding: 12px 24px; font-size: 16px; font-weight: 600; text-decoration: none; border-radius: 6px; text-align: center;`\n \n return `\n <div style=\"margin: 0 0 16px 0; text-align: center;\">\n <a href=\"${escapeHtml(url)}\" target=\"_blank\" rel=\"noopener noreferrer\" style=\"${buttonStyle}\">${escapeHtml(text)}</a>\n </div>\n `\n}\n\n/**\n * Convert divider block\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction convertDividerBlock(fields: any): string {\n const style = fields?.style || 'solid'\n \n const styles: Record<string, string> = {\n solid: 'border-top: 1px solid #e5e7eb;',\n dashed: 'border-top: 1px dashed #e5e7eb;',\n dotted: 'border-top: 1px dotted #e5e7eb;',\n }\n \n return `<hr style=\"${styles[style] || styles.solid} margin: 24px 0; border-bottom: none; border-left: none; border-right: none;\" />`\n}\n\n/**\n * Get text alignment from format number\n */\nfunction getAlignment(format?: number): string {\n if (!format) return 'left'\n \n // Lexical alignment format values\n if (format & 2) return 'center'\n if (format & 3) return 'right'\n if (format & 4) return 'justify'\n \n return 'left'\n}\n\n/**\n * Escape HTML special characters\n */\nfunction escapeHtml(text: string): string {\n const map: Record<string, string> = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": '''\n }\n \n return text.replace(/[&<>\"']/g, m => map[m])\n}\n\n/**\n * Wrap content in a responsive email template\n */\nfunction wrapInEmailTemplate(content: string, preheader?: string): string {\n return `<!DOCTYPE html>\n<html lang=\"en\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n <meta name=\"x-apple-disable-message-reformatting\">\n <title>Newsletter</title>\n \n <!--[if mso]>\n <noscript>\n <xml>\n <o:OfficeDocumentSettings>\n <o:PixelsPerInch>96</o:PixelsPerInch>\n </o:OfficeDocumentSettings>\n </xml>\n </noscript>\n <![endif]-->\n \n <style>\n /* Reset and base styles */\n * {\n -webkit-text-size-adjust: 100%;\n -ms-text-size-adjust: 100%;\n }\n \n body {\n margin: 0 !important;\n padding: 0 !important;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;\n font-size: 16px;\n line-height: 1.5;\n color: #1A1A1A;\n background-color: #f8f9fa;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n }\n \n table {\n border-spacing: 0 !important;\n border-collapse: collapse !important;\n table-layout: fixed !important;\n margin: 0 auto !important;\n }\n \n table table table {\n table-layout: auto;\n }\n \n img {\n -ms-interpolation-mode: bicubic;\n max-width: 100%;\n height: auto;\n border: 0;\n outline: none;\n text-decoration: none;\n }\n \n /* Responsive styles */\n @media only screen and (max-width: 640px) {\n .mobile-hide {\n display: none !important;\n }\n \n .mobile-center {\n text-align: center !important;\n }\n \n .mobile-width-100 {\n width: 100% !important;\n max-width: 100% !important;\n }\n \n .mobile-padding {\n padding: 20px !important;\n }\n \n .mobile-padding-sm {\n padding: 16px !important;\n }\n \n .mobile-font-size-14 {\n font-size: 14px !important;\n }\n \n .mobile-font-size-16 {\n font-size: 16px !important;\n }\n \n .mobile-font-size-20 {\n font-size: 20px !important;\n line-height: 1.3 !important;\n }\n \n .mobile-font-size-24 {\n font-size: 24px !important;\n line-height: 1.2 !important;\n }\n \n /* Stack sections on mobile */\n .mobile-stack {\n display: block !important;\n width: 100% !important;\n }\n \n /* Mobile-specific spacing */\n .mobile-margin-bottom-16 {\n margin-bottom: 16px !important;\n }\n \n .mobile-margin-bottom-20 {\n margin-bottom: 20px !important;\n }\n }\n \n /* Dark mode support */\n @media (prefers-color-scheme: dark) {\n .dark-mode-bg {\n background-color: #1a1a1a !important;\n }\n \n .dark-mode-text {\n color: #ffffff !important;\n }\n \n .dark-mode-border {\n border-color: #333333 !important;\n }\n }\n \n /* Outlook-specific fixes */\n <!--[if mso]>\n <style>\n table {\n border-collapse: collapse;\n border-spacing: 0;\n border: none;\n margin: 0;\n }\n \n div, p {\n margin: 0;\n }\n </style>\n <![endif]-->\n </style>\n</head>\n<body style=\"margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.5; color: #1A1A1A; background-color: #f8f9fa;\">\n ${preheader ? `\n <!-- Preheader text -->\n <div style=\"display: none; max-height: 0; overflow: hidden; font-size: 1px; line-height: 1px; color: transparent;\">\n ${escapeHtml(preheader)}\n </div>\n ` : ''}\n \n <!-- Main container -->\n <table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\" style=\"margin: 0; padding: 0; background-color: #f8f9fa;\">\n <tr>\n <td align=\"center\" style=\"padding: 20px 10px;\">\n <!-- Email wrapper -->\n <table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" width=\"600\" class=\"mobile-width-100\" style=\"margin: 0 auto; max-width: 600px;\">\n <tr>\n <td class=\"mobile-padding\" style=\"padding: 0;\">\n <!-- Content area with light background -->\n <div style=\"background-color: #ffffff; padding: 40px 30px; border-radius: 8px;\" class=\"mobile-padding\">\n ${content}\n </div>\n </td>\n </tr>\n </table>\n </td>\n </tr>\n </table>\n</body>\n</html>`\n}\n\n/**\n * Extract personalization tags from content\n */\nexport function extractPersonalizationTags(html: string): string[] {\n const regex = /\\{\\{([^}]+)\\}\\}/g\n const tags: string[] = []\n let match\n \n while ((match = regex.exec(html)) !== null) {\n tags.push(match[1].trim())\n }\n \n return [...new Set(tags)]\n}\n\n/**\n * Replace personalization tags with sample data\n */\nexport function replacePersonalizationTags(\n html: string, \n sampleData: Record<string, string>\n): string {\n return html.replace(/\\{\\{([^}]+)\\}\\}/g, (match, tag) => {\n const trimmedTag = tag.trim()\n return sampleData[trimmedTag] || match\n })\n}","/**\n * Email HTML validation utilities\n */\n\nexport interface ValidationResult {\n valid: boolean\n warnings: string[]\n errors: string[]\n stats: {\n sizeInBytes: number\n imageCount: number\n linkCount: number\n hasExternalStyles: boolean\n hasJavaScript: boolean\n }\n}\n\n/**\n * Validate HTML for email compatibility\n */\nexport function validateEmailHtml(html: string): ValidationResult {\n const warnings: string[] = []\n const errors: string[] = []\n \n // Calculate size\n const sizeInBytes = new Blob([html]).size\n \n // Check size limits\n if (sizeInBytes > 102400) { // 100KB\n warnings.push(`Email size (${Math.round(sizeInBytes / 1024)}KB) exceeds Gmail's 102KB limit - email may be clipped`)\n }\n \n // Check for problematic CSS\n if (html.includes('position:') && (html.includes('position: absolute') || html.includes('position: fixed'))) {\n errors.push('Absolute/fixed positioning is not supported in most email clients')\n }\n \n if (html.includes('display: flex') || html.includes('display: grid')) {\n errors.push('Flexbox and Grid layouts are not supported in many email clients')\n }\n \n if (html.includes('@media')) {\n warnings.push('Media queries may not work in all email clients')\n }\n \n // Check for JavaScript\n const hasJavaScript = \n html.includes('<script') || \n html.includes('onclick') || \n html.includes('onload') ||\n html.includes('javascript:')\n \n if (hasJavaScript) {\n errors.push('JavaScript is not supported in email and will be stripped by email clients')\n }\n \n // Check for external styles\n const hasExternalStyles = html.includes('<link') && html.includes('stylesheet')\n if (hasExternalStyles) {\n errors.push('External stylesheets are not supported - use inline styles only')\n }\n \n // Check for forms\n if (html.includes('<form') || html.includes('<input') || html.includes('<button')) {\n errors.push('Forms and form elements are not reliably supported in email')\n }\n \n // Check for unsupported tags\n const unsupportedTags = [\n 'video', 'audio', 'iframe', 'embed', 'object', 'canvas', 'svg'\n ]\n \n for (const tag of unsupportedTags) {\n if (html.includes(`<${tag}`)) {\n errors.push(`<${tag}> tags are not supported in email`)\n }\n }\n \n // Count images and links\n const imageCount = (html.match(/<img/g) || []).length\n const linkCount = (html.match(/<a/g) || []).length\n \n // Check image usage\n if (imageCount > 20) {\n warnings.push(`High number of images (${imageCount}) may affect email performance`)\n }\n \n // Check for missing alt text\n const imagesWithoutAlt = (html.match(/<img(?![^>]*\\balt\\s*=)[^>]*>/g) || []).length\n if (imagesWithoutAlt > 0) {\n warnings.push(`${imagesWithoutAlt} image(s) missing alt text - important for accessibility`)\n }\n \n // Check for proper link attributes\n const linksWithoutTarget = (html.match(/<a(?![^>]*\\btarget\\s*=)[^>]*>/g) || []).length\n if (linksWithoutTarget > 0) {\n warnings.push(`${linksWithoutTarget} link(s) missing target=\"_blank\" attribute`)\n }\n \n // Check for CSS property usage\n if (html.includes('margin: auto') || html.includes('margin:auto')) {\n warnings.push('margin: auto is not supported in Outlook - use align=\"center\" or tables for centering')\n }\n \n if (html.includes('background-image')) {\n warnings.push('Background images are not reliably supported - consider using <img> tags instead')\n }\n \n // Check for rem/em units\n if (html.match(/\\d+\\s*(rem|em)/)) {\n warnings.push('rem/em units may render inconsistently - use px for reliable sizing')\n }\n \n // Check for negative margins\n if (html.match(/margin[^:]*:\\s*-\\d+/)) {\n errors.push('Negative margins are not supported in many email clients')\n }\n \n // Validate personalization tags\n const personalizationTags = html.match(/\\{\\{([^}]+)\\}\\}/g) || []\n const validTags = ['subscriber.name', 'subscriber.email', 'subscriber.firstName', 'subscriber.lastName']\n \n for (const tag of personalizationTags) {\n const tagContent = tag.replace(/[{}]/g, '').trim()\n if (!validTags.includes(tagContent)) {\n warnings.push(`Unknown personalization tag: ${tag}`)\n }\n }\n \n return {\n valid: errors.length === 0,\n warnings,\n errors,\n stats: {\n sizeInBytes,\n imageCount,\n linkCount,\n hasExternalStyles,\n hasJavaScript,\n }\n }\n}\n\n/**\n * Get email client compatibility warnings for specific HTML\n */\nexport function getClientCompatibilityWarnings(html: string): Record<string, string[]> {\n const warnings: Record<string, string[]> = {\n gmail: [],\n outlook: [],\n appleMail: [],\n mobile: [],\n }\n \n // Gmail specific\n if (html.includes('<style')) {\n warnings.gmail.push('Gmail may strip <style> tags in some contexts')\n }\n \n // Outlook specific\n if (html.includes('margin: auto') || html.includes('margin:auto')) {\n warnings.outlook.push('Outlook does not support margin: auto')\n }\n \n if (html.includes('padding') && html.includes('<p')) {\n warnings.outlook.push('Outlook may not respect padding on <p> tags')\n }\n \n if (html.includes('background-image')) {\n warnings.outlook.push('Outlook has limited background image support')\n }\n \n // Mobile specific\n const hasSmallText = html.match(/font-size:\\s*(\\d+)px/g)?.some(match => {\n const size = parseInt(match.match(/\\d+/)?.[0] || '16')\n return size < 14\n })\n \n if (hasSmallText) {\n warnings.mobile.push('Text smaller than 14px may be hard to read on mobile')\n }\n \n const hasSmallLinks = html.match(/<a[^>]*>[^<]{1,3}<\\/a>/g)\n if (hasSmallLinks) {\n warnings.mobile.push('Short link text may be hard to tap on mobile devices')\n }\n \n return warnings\n}\n\n/**\n * Suggest fixes for common email HTML issues\n */\nexport function suggestFixes(html: string): string[] {\n const suggestions: string[] = []\n \n if (html.includes('display: flex')) {\n suggestions.push('Replace flexbox with table-based layouts for better email client support')\n }\n \n if (html.includes('position: absolute')) {\n suggestions.push('Use table cells or margins instead of absolute positioning')\n }\n \n if (html.match(/\\d+rem/) || html.match(/\\d+em/)) {\n suggestions.push('Convert rem/em units to px for consistent rendering')\n }\n \n if (!html.includes('<!DOCTYPE')) {\n suggestions.push('Add <!DOCTYPE html> declaration for better rendering')\n }\n \n if (!html.includes('charset')) {\n suggestions.push('Add <meta charset=\"UTF-8\"> for proper character encoding')\n }\n \n return suggestions\n}","import type { PayloadRequest } from 'payload'\nimport type { NewsletterPluginConfig, BroadcastProviderConfig } from '../types'\n\nexport async function getBroadcastConfig(\n req: PayloadRequest,\n pluginConfig: NewsletterPluginConfig\n): Promise<BroadcastProviderConfig | null> {\n try {\n // Get settings from Newsletter Settings collection\n const settings = await req.payload.findGlobal({\n slug: pluginConfig.settingsSlug || 'newsletter-settings',\n req,\n })\n\n // Build provider config from settings, falling back to env vars\n if (settings?.provider === 'broadcast' && settings?.broadcastSettings) {\n return {\n apiUrl: settings.broadcastSettings.apiUrl || pluginConfig.providers?.broadcast?.apiUrl || '',\n token: settings.broadcastSettings.token || pluginConfig.providers?.broadcast?.token || '',\n fromAddress: settings.fromAddress || pluginConfig.providers?.broadcast?.fromAddress || '',\n fromName: settings.fromName || pluginConfig.providers?.broadcast?.fromName || '',\n replyTo: settings.replyTo || pluginConfig.providers?.broadcast?.replyTo,\n }\n }\n\n // Fall back to env var config\n return pluginConfig.providers?.broadcast || null\n } catch (error) {\n req.payload.logger.error({ error: String(error) }, 'Failed to get broadcast config from settings')\n // Fall back to env var config on error\n return pluginConfig.providers?.broadcast || null\n }\n}","import type { PayloadRequest } from 'payload'\nimport type { NewsletterPluginConfig, ResendProviderConfig } from '../types'\n\nexport async function getResendConfig(\n req: PayloadRequest,\n pluginConfig: NewsletterPluginConfig\n): Promise<ResendProviderConfig | null> {\n try {\n // Get settings from Newsletter Settings collection\n const settings = await req.payload.findGlobal({\n slug: pluginConfig.settingsSlug || 'newsletter-settings',\n req,\n })\n\n // Build provider config from settings, falling back to env vars\n if (settings?.provider === 'resend' && settings?.resendSettings) {\n return {\n apiKey: settings.resendSettings.apiKey || pluginConfig.providers?.resend?.apiKey || '',\n fromAddress: settings.fromAddress || pluginConfig.providers?.resend?.fromAddress || '',\n fromName: settings.fromName || pluginConfig.providers?.resend?.fromName || '',\n audienceIds: settings.resendSettings.audienceIds || pluginConfig.providers?.resend?.audienceIds,\n }\n }\n\n // Fall back to env var config\n return pluginConfig.providers?.resend || null\n } catch (error) {\n req.payload.logger.error({ error: String(error) }, 'Failed to get resend config from settings')\n // Fall back to env var config on error\n return pluginConfig.providers?.resend || null\n }\n}"],"mappings":"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","names":["DOMPurify"]}
|
package/dist/utils.js
CHANGED
|
@@ -581,7 +581,7 @@ async function getBroadcastConfig(req, pluginConfig) {
|
|
|
581
581
|
}
|
|
582
582
|
return pluginConfig.providers?.broadcast || null;
|
|
583
583
|
} catch (error) {
|
|
584
|
-
req.payload.logger.error("Failed to get broadcast config from settings
|
|
584
|
+
req.payload.logger.error({ error: String(error) }, "Failed to get broadcast config from settings");
|
|
585
585
|
return pluginConfig.providers?.broadcast || null;
|
|
586
586
|
}
|
|
587
587
|
}
|
|
@@ -603,7 +603,7 @@ async function getResendConfig(req, pluginConfig) {
|
|
|
603
603
|
}
|
|
604
604
|
return pluginConfig.providers?.resend || null;
|
|
605
605
|
} catch (error) {
|
|
606
|
-
req.payload.logger.error("Failed to get resend config from settings
|
|
606
|
+
req.payload.logger.error({ error: String(error) }, "Failed to get resend config from settings");
|
|
607
607
|
return pluginConfig.providers?.resend || null;
|
|
608
608
|
}
|
|
609
609
|
}
|