payload-plugin-newsletter 0.17.1 → 0.17.3
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 +25 -0
- package/dist/collections.cjs +432 -62
- package/dist/collections.cjs.map +1 -1
- package/dist/collections.js +432 -62
- package/dist/collections.js.map +1 -1
- package/dist/index.cjs +1017 -1019
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1057 -1059
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -2873,270 +2873,660 @@ var createSignoutEndpoint = (_config) => {
|
|
|
2873
2873
|
};
|
|
2874
2874
|
};
|
|
2875
2875
|
|
|
2876
|
-
// src/endpoints/broadcasts/
|
|
2877
|
-
|
|
2876
|
+
// src/endpoints/broadcasts/index.ts
|
|
2877
|
+
var createBroadcastManagementEndpoints = (config) => {
|
|
2878
|
+
return [];
|
|
2879
|
+
};
|
|
2878
2880
|
|
|
2879
|
-
// src/
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
} catch {
|
|
2895
|
-
return null;
|
|
2896
|
-
}
|
|
2897
|
-
}
|
|
2898
|
-
async function requireAdmin(req, config) {
|
|
2899
|
-
const user = await getAuthenticatedUser(req);
|
|
2900
|
-
if (!user) {
|
|
2901
|
-
return {
|
|
2902
|
-
authorized: false,
|
|
2903
|
-
error: "Authentication required"
|
|
2904
|
-
};
|
|
2905
|
-
}
|
|
2906
|
-
if (!isAdmin(user, config)) {
|
|
2907
|
-
return {
|
|
2908
|
-
authorized: false,
|
|
2909
|
-
error: "Admin access required"
|
|
2910
|
-
};
|
|
2881
|
+
// src/endpoints/index.ts
|
|
2882
|
+
function createNewsletterEndpoints(config) {
|
|
2883
|
+
const endpoints = [
|
|
2884
|
+
createSubscribeEndpoint(config),
|
|
2885
|
+
createUnsubscribeEndpoint(config)
|
|
2886
|
+
];
|
|
2887
|
+
if (config.auth?.enabled !== false) {
|
|
2888
|
+
endpoints.push(
|
|
2889
|
+
createVerifyMagicLinkEndpoint(config),
|
|
2890
|
+
createPreferencesEndpoint(config),
|
|
2891
|
+
createUpdatePreferencesEndpoint(config),
|
|
2892
|
+
createSigninEndpoint(config),
|
|
2893
|
+
createMeEndpoint(config),
|
|
2894
|
+
createSignoutEndpoint(config)
|
|
2895
|
+
);
|
|
2911
2896
|
}
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
user
|
|
2915
|
-
};
|
|
2897
|
+
endpoints.push(...createBroadcastManagementEndpoints(config));
|
|
2898
|
+
return endpoints;
|
|
2916
2899
|
}
|
|
2917
2900
|
|
|
2918
|
-
// src/
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2901
|
+
// src/fields/newsletterScheduling.ts
|
|
2902
|
+
function createNewsletterSchedulingFields(config) {
|
|
2903
|
+
const groupName = config.features?.newsletterScheduling?.fields?.groupName || "newsletterScheduling";
|
|
2904
|
+
const contentField = config.features?.newsletterScheduling?.fields?.contentField || "content";
|
|
2905
|
+
const createMarkdownField = config.features?.newsletterScheduling?.fields?.createMarkdownField !== false;
|
|
2906
|
+
const fields = [
|
|
2907
|
+
{
|
|
2908
|
+
name: groupName,
|
|
2909
|
+
type: "group",
|
|
2910
|
+
label: "Newsletter Scheduling",
|
|
2911
|
+
admin: {
|
|
2912
|
+
condition: (data, { user }) => user?.collection === "users"
|
|
2913
|
+
// Only show for admin users
|
|
2914
|
+
},
|
|
2915
|
+
fields: [
|
|
2916
|
+
{
|
|
2917
|
+
name: "scheduled",
|
|
2918
|
+
type: "checkbox",
|
|
2919
|
+
label: "Schedule for Newsletter",
|
|
2920
|
+
defaultValue: false,
|
|
2921
|
+
admin: {
|
|
2922
|
+
description: "Schedule this content to be sent as a newsletter"
|
|
2923
|
+
}
|
|
2924
|
+
},
|
|
2925
|
+
{
|
|
2926
|
+
name: "scheduledDate",
|
|
2927
|
+
type: "date",
|
|
2928
|
+
label: "Send Date",
|
|
2929
|
+
required: true,
|
|
2930
|
+
admin: {
|
|
2931
|
+
date: {
|
|
2932
|
+
pickerAppearance: "dayAndTime"
|
|
2933
|
+
},
|
|
2934
|
+
condition: (data) => data?.[groupName]?.scheduled,
|
|
2935
|
+
description: "When to send this newsletter"
|
|
2936
|
+
}
|
|
2937
|
+
},
|
|
2938
|
+
{
|
|
2939
|
+
name: "sentDate",
|
|
2940
|
+
type: "date",
|
|
2941
|
+
label: "Sent Date",
|
|
2942
|
+
admin: {
|
|
2943
|
+
readOnly: true,
|
|
2944
|
+
condition: (data) => data?.[groupName]?.sendStatus === "sent",
|
|
2945
|
+
description: "When this newsletter was sent"
|
|
2946
|
+
}
|
|
2947
|
+
},
|
|
2948
|
+
{
|
|
2949
|
+
name: "sendStatus",
|
|
2950
|
+
type: "select",
|
|
2951
|
+
label: "Status",
|
|
2952
|
+
options: [
|
|
2953
|
+
{ label: "Draft", value: "draft" },
|
|
2954
|
+
{ label: "Scheduled", value: "scheduled" },
|
|
2955
|
+
{ label: "Sending", value: "sending" },
|
|
2956
|
+
{ label: "Sent", value: "sent" },
|
|
2957
|
+
{ label: "Failed", value: "failed" }
|
|
2958
|
+
],
|
|
2959
|
+
defaultValue: "draft",
|
|
2960
|
+
admin: {
|
|
2961
|
+
readOnly: true,
|
|
2962
|
+
description: "Current send status"
|
|
2963
|
+
}
|
|
2964
|
+
},
|
|
2965
|
+
{
|
|
2966
|
+
name: "emailSubject",
|
|
2967
|
+
type: "text",
|
|
2968
|
+
label: "Email Subject",
|
|
2969
|
+
required: true,
|
|
2970
|
+
admin: {
|
|
2971
|
+
condition: (data) => data?.[groupName]?.scheduled,
|
|
2972
|
+
description: "Subject line for the newsletter email"
|
|
2973
|
+
}
|
|
2974
|
+
},
|
|
2975
|
+
{
|
|
2976
|
+
name: "preheader",
|
|
2977
|
+
type: "text",
|
|
2978
|
+
label: "Email Preheader",
|
|
2979
|
+
admin: {
|
|
2980
|
+
condition: (data) => data?.[groupName]?.scheduled,
|
|
2981
|
+
description: "Preview text that appears after the subject line"
|
|
2982
|
+
}
|
|
2983
|
+
},
|
|
2984
|
+
{
|
|
2985
|
+
name: "segments",
|
|
2986
|
+
type: "select",
|
|
2987
|
+
label: "Target Segments",
|
|
2988
|
+
hasMany: true,
|
|
2989
|
+
options: [
|
|
2990
|
+
{ label: "All Subscribers", value: "all" },
|
|
2991
|
+
...config.i18n?.locales?.map((locale) => ({
|
|
2992
|
+
label: `${locale.toUpperCase()} Subscribers`,
|
|
2993
|
+
value: locale
|
|
2994
|
+
})) || []
|
|
2995
|
+
],
|
|
2996
|
+
defaultValue: ["all"],
|
|
2997
|
+
admin: {
|
|
2998
|
+
condition: (data) => data?.[groupName]?.scheduled,
|
|
2999
|
+
description: "Which subscriber segments to send to"
|
|
3000
|
+
}
|
|
3001
|
+
},
|
|
3002
|
+
{
|
|
3003
|
+
name: "testEmails",
|
|
3004
|
+
type: "array",
|
|
3005
|
+
label: "Test Email Recipients",
|
|
3006
|
+
admin: {
|
|
3007
|
+
condition: (data) => data?.[groupName]?.scheduled && data?.[groupName]?.sendStatus === "draft",
|
|
3008
|
+
description: "Send test emails before scheduling"
|
|
3009
|
+
},
|
|
3010
|
+
fields: [
|
|
3011
|
+
{
|
|
3012
|
+
name: "email",
|
|
3013
|
+
type: "email",
|
|
3014
|
+
required: true
|
|
3015
|
+
}
|
|
3016
|
+
]
|
|
3017
|
+
}
|
|
3018
|
+
]
|
|
2933
3019
|
}
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
3020
|
+
];
|
|
3021
|
+
if (createMarkdownField) {
|
|
3022
|
+
fields.push(createMarkdownFieldInternal({
|
|
3023
|
+
name: `${contentField}Markdown`,
|
|
3024
|
+
richTextField: contentField,
|
|
3025
|
+
label: "Email Content (Markdown)",
|
|
3026
|
+
admin: {
|
|
3027
|
+
position: "sidebar",
|
|
3028
|
+
condition: (data) => Boolean(data?.[contentField] && data?.[groupName]?.scheduled),
|
|
3029
|
+
description: "Markdown version for email rendering",
|
|
3030
|
+
readOnly: true
|
|
3031
|
+
}
|
|
3032
|
+
}));
|
|
2938
3033
|
}
|
|
3034
|
+
return fields;
|
|
2939
3035
|
}
|
|
2940
|
-
|
|
2941
|
-
// src/endpoints/broadcasts/send.ts
|
|
2942
|
-
var createSendBroadcastEndpoint = (config, collectionSlug) => {
|
|
3036
|
+
function createMarkdownFieldInternal(config) {
|
|
2943
3037
|
return {
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
3038
|
+
name: config.name,
|
|
3039
|
+
type: "textarea",
|
|
3040
|
+
label: config.label || "Markdown",
|
|
3041
|
+
admin: {
|
|
3042
|
+
...config.admin,
|
|
3043
|
+
description: config.admin?.description || "Auto-generated from rich text content"
|
|
3044
|
+
},
|
|
3045
|
+
hooks: {
|
|
3046
|
+
afterRead: [
|
|
3047
|
+
async ({ data }) => {
|
|
3048
|
+
if (data?.[config.richTextField]) {
|
|
3049
|
+
try {
|
|
3050
|
+
const { convertLexicalToMarkdown } = await import("@payloadcms/richtext-lexical");
|
|
3051
|
+
return convertLexicalToMarkdown({
|
|
3052
|
+
data: data[config.richTextField]
|
|
3053
|
+
});
|
|
3054
|
+
} catch {
|
|
3055
|
+
return "";
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
return "";
|
|
2954
3059
|
}
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
}, { status: 400 });
|
|
3060
|
+
],
|
|
3061
|
+
beforeChange: [
|
|
3062
|
+
() => {
|
|
3063
|
+
return null;
|
|
2960
3064
|
}
|
|
2961
|
-
|
|
2962
|
-
const pathParts = url.pathname.split("/");
|
|
2963
|
-
const id = pathParts[pathParts.length - 2];
|
|
2964
|
-
if (!id) {
|
|
2965
|
-
return Response.json({
|
|
2966
|
-
success: false,
|
|
2967
|
-
error: "Broadcast ID is required"
|
|
2968
|
-
}, { status: 400 });
|
|
2969
|
-
}
|
|
2970
|
-
const data = await (req.json?.() || Promise.resolve({}));
|
|
2971
|
-
const broadcastDoc = await req.payload.findByID({
|
|
2972
|
-
collection: collectionSlug,
|
|
2973
|
-
id,
|
|
2974
|
-
user: auth.user
|
|
2975
|
-
});
|
|
2976
|
-
if (!broadcastDoc || !broadcastDoc.providerId) {
|
|
2977
|
-
return Response.json({
|
|
2978
|
-
success: false,
|
|
2979
|
-
error: "Broadcast not found or not synced with provider"
|
|
2980
|
-
}, { status: 404 });
|
|
2981
|
-
}
|
|
2982
|
-
const providerConfig = await getBroadcastConfig(req, config);
|
|
2983
|
-
if (!providerConfig || !providerConfig.token) {
|
|
2984
|
-
return Response.json({
|
|
2985
|
-
success: false,
|
|
2986
|
-
error: "Broadcast provider not configured in Newsletter Settings or environment variables"
|
|
2987
|
-
}, { status: 500 });
|
|
2988
|
-
}
|
|
2989
|
-
const { BroadcastApiProvider: BroadcastApiProvider2 } = await Promise.resolve().then(() => (init_broadcast2(), broadcast_exports));
|
|
2990
|
-
const provider = new BroadcastApiProvider2(providerConfig);
|
|
2991
|
-
const broadcast = await provider.send(broadcastDoc.providerId, data);
|
|
2992
|
-
await req.payload.update({
|
|
2993
|
-
collection: collectionSlug,
|
|
2994
|
-
id,
|
|
2995
|
-
data: {
|
|
2996
|
-
sendStatus: "sending" /* SENDING */,
|
|
2997
|
-
sentAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2998
|
-
},
|
|
2999
|
-
user: auth.user
|
|
3000
|
-
});
|
|
3001
|
-
return Response.json({
|
|
3002
|
-
success: true,
|
|
3003
|
-
message: "Broadcast sent successfully",
|
|
3004
|
-
broadcast
|
|
3005
|
-
});
|
|
3006
|
-
} catch (error) {
|
|
3007
|
-
console.error("Failed to send broadcast:", error);
|
|
3008
|
-
if (error instanceof NewsletterProviderError) {
|
|
3009
|
-
return Response.json({
|
|
3010
|
-
success: false,
|
|
3011
|
-
error: error.message,
|
|
3012
|
-
code: error.code
|
|
3013
|
-
}, { status: error.code === "NOT_SUPPORTED" ? 501 : 500 });
|
|
3014
|
-
}
|
|
3015
|
-
return Response.json({
|
|
3016
|
-
success: false,
|
|
3017
|
-
error: "Failed to send broadcast"
|
|
3018
|
-
}, { status: 500 });
|
|
3019
|
-
}
|
|
3065
|
+
]
|
|
3020
3066
|
}
|
|
3021
3067
|
};
|
|
3022
|
-
}
|
|
3068
|
+
}
|
|
3023
3069
|
|
|
3024
|
-
// src/
|
|
3025
|
-
|
|
3026
|
-
var createScheduleBroadcastEndpoint = (config, collectionSlug) => {
|
|
3070
|
+
// src/jobs/sync-unsubscribes.ts
|
|
3071
|
+
var createUnsubscribeSyncJob = (pluginConfig) => {
|
|
3027
3072
|
return {
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
handler: async (req) => {
|
|
3073
|
+
slug: "sync-unsubscribes",
|
|
3074
|
+
label: "Sync Unsubscribes from Email Service",
|
|
3075
|
+
handler: async ({ req }) => {
|
|
3076
|
+
const subscribersSlug = pluginConfig.subscribersSlug || "subscribers";
|
|
3077
|
+
const emailService = req.payload.newsletterEmailService;
|
|
3078
|
+
if (!emailService) {
|
|
3079
|
+
console.error("Email service not configured");
|
|
3080
|
+
return {
|
|
3081
|
+
output: {
|
|
3082
|
+
syncedCount: 0
|
|
3083
|
+
}
|
|
3084
|
+
};
|
|
3085
|
+
}
|
|
3086
|
+
let syncedCount = 0;
|
|
3031
3087
|
try {
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
}
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3088
|
+
if (emailService.getProvider() === "broadcast") {
|
|
3089
|
+
console.warn("Starting Broadcast unsubscribe sync...");
|
|
3090
|
+
const broadcastConfig = pluginConfig.providers?.broadcast;
|
|
3091
|
+
if (!broadcastConfig) {
|
|
3092
|
+
throw new Error("Broadcast configuration not found");
|
|
3093
|
+
}
|
|
3094
|
+
const apiUrl = broadcastConfig.apiUrl.replace(/\/$/, "");
|
|
3095
|
+
const token = broadcastConfig.token;
|
|
3096
|
+
let page = 1;
|
|
3097
|
+
let hasMore = true;
|
|
3098
|
+
while (hasMore) {
|
|
3099
|
+
const response = await fetch(
|
|
3100
|
+
`${apiUrl}/api/v1/subscribers.json?page=${page}`,
|
|
3101
|
+
{
|
|
3102
|
+
headers: {
|
|
3103
|
+
"Authorization": `Bearer ${token}`
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
);
|
|
3107
|
+
if (!response.ok) {
|
|
3108
|
+
throw new Error(`Broadcast API error: ${response.status}`);
|
|
3109
|
+
}
|
|
3110
|
+
const data = await response.json();
|
|
3111
|
+
const broadcastSubscribers = data.subscribers || [];
|
|
3112
|
+
for (const broadcastSub of broadcastSubscribers) {
|
|
3113
|
+
const payloadSubscribers = await req.payload.find({
|
|
3114
|
+
collection: subscribersSlug,
|
|
3115
|
+
where: {
|
|
3116
|
+
email: {
|
|
3117
|
+
equals: broadcastSub.email
|
|
3118
|
+
}
|
|
3119
|
+
},
|
|
3120
|
+
limit: 1
|
|
3121
|
+
});
|
|
3122
|
+
if (payloadSubscribers.docs.length > 0) {
|
|
3123
|
+
const payloadSub = payloadSubscribers.docs[0];
|
|
3124
|
+
const broadcastUnsubscribed = !broadcastSub.is_active || broadcastSub.unsubscribed_at;
|
|
3125
|
+
const payloadUnsubscribed = payloadSub.subscriptionStatus === "unsubscribed";
|
|
3126
|
+
if (broadcastUnsubscribed && !payloadUnsubscribed) {
|
|
3127
|
+
await req.payload.update({
|
|
3128
|
+
collection: subscribersSlug,
|
|
3129
|
+
id: payloadSub.id,
|
|
3130
|
+
data: {
|
|
3131
|
+
subscriptionStatus: "unsubscribed",
|
|
3132
|
+
unsubscribedAt: broadcastSub.unsubscribed_at || (/* @__PURE__ */ new Date()).toISOString()
|
|
3133
|
+
}
|
|
3134
|
+
});
|
|
3135
|
+
syncedCount++;
|
|
3136
|
+
console.warn(`Unsubscribed: ${broadcastSub.email}`);
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
3139
|
+
}
|
|
3140
|
+
if (data.pagination && data.pagination.current < data.pagination.total_pages) {
|
|
3141
|
+
page++;
|
|
3142
|
+
} else {
|
|
3143
|
+
hasMore = false;
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
console.warn(`Broadcast sync complete. Unsubscribed ${syncedCount} contacts.`);
|
|
3074
3147
|
}
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
success: false,
|
|
3083
|
-
error: "Broadcast not found or not synced with provider"
|
|
3084
|
-
}, { status: 404 });
|
|
3148
|
+
if (emailService.getProvider() === "resend") {
|
|
3149
|
+
console.warn("Starting Resend unsubscribe sync...");
|
|
3150
|
+
const resendConfig = pluginConfig.providers?.resend;
|
|
3151
|
+
if (!resendConfig) {
|
|
3152
|
+
throw new Error("Resend configuration not found");
|
|
3153
|
+
}
|
|
3154
|
+
console.warn("Resend polling implementation needed - webhooks recommended");
|
|
3085
3155
|
}
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
}, { status: 500 });
|
|
3156
|
+
if (pluginConfig.hooks?.afterUnsubscribeSync) {
|
|
3157
|
+
await pluginConfig.hooks.afterUnsubscribeSync({
|
|
3158
|
+
req,
|
|
3159
|
+
syncedCount
|
|
3160
|
+
});
|
|
3092
3161
|
}
|
|
3093
|
-
const { BroadcastApiProvider: BroadcastApiProvider2 } = await Promise.resolve().then(() => (init_broadcast2(), broadcast_exports));
|
|
3094
|
-
const provider = new BroadcastApiProvider2(providerConfig);
|
|
3095
|
-
const broadcast = await provider.schedule(broadcastDoc.providerId, scheduledDate);
|
|
3096
|
-
await req.payload.update({
|
|
3097
|
-
collection: collectionSlug,
|
|
3098
|
-
id,
|
|
3099
|
-
data: {
|
|
3100
|
-
sendStatus: "scheduled" /* SCHEDULED */,
|
|
3101
|
-
scheduledAt: scheduledDate.toISOString()
|
|
3102
|
-
},
|
|
3103
|
-
user: auth.user
|
|
3104
|
-
});
|
|
3105
|
-
return Response.json({
|
|
3106
|
-
success: true,
|
|
3107
|
-
message: `Broadcast scheduled for ${scheduledDate.toISOString()}`,
|
|
3108
|
-
broadcast
|
|
3109
|
-
});
|
|
3110
3162
|
} catch (error) {
|
|
3111
|
-
console.error("
|
|
3112
|
-
|
|
3113
|
-
return Response.json({
|
|
3114
|
-
success: false,
|
|
3115
|
-
error: error.message,
|
|
3116
|
-
code: error.code
|
|
3117
|
-
}, { status: error.code === "NOT_SUPPORTED" ? 501 : 500 });
|
|
3118
|
-
}
|
|
3119
|
-
return Response.json({
|
|
3120
|
-
success: false,
|
|
3121
|
-
error: "Failed to schedule broadcast"
|
|
3122
|
-
}, { status: 500 });
|
|
3163
|
+
console.error("Unsubscribe sync error:", error);
|
|
3164
|
+
throw error;
|
|
3123
3165
|
}
|
|
3166
|
+
return {
|
|
3167
|
+
output: {
|
|
3168
|
+
syncedCount
|
|
3169
|
+
}
|
|
3170
|
+
};
|
|
3124
3171
|
}
|
|
3125
3172
|
};
|
|
3126
3173
|
};
|
|
3127
3174
|
|
|
3128
|
-
// src/
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3175
|
+
// src/collections/Broadcasts.ts
|
|
3176
|
+
init_types();
|
|
3177
|
+
|
|
3178
|
+
// src/fields/emailContent.ts
|
|
3179
|
+
var import_richtext_lexical = require("@payloadcms/richtext-lexical");
|
|
3180
|
+
|
|
3181
|
+
// src/utils/blockValidation.ts
|
|
3182
|
+
var EMAIL_INCOMPATIBLE_TYPES = [
|
|
3183
|
+
"chart",
|
|
3184
|
+
"dataTable",
|
|
3185
|
+
"interactive",
|
|
3186
|
+
"streamable",
|
|
3187
|
+
"video",
|
|
3188
|
+
"iframe",
|
|
3189
|
+
"form",
|
|
3190
|
+
"carousel",
|
|
3191
|
+
"tabs",
|
|
3192
|
+
"accordion",
|
|
3193
|
+
"map"
|
|
3194
|
+
];
|
|
3195
|
+
var validateEmailBlocks = (blocks) => {
|
|
3196
|
+
blocks.forEach((block) => {
|
|
3197
|
+
if (EMAIL_INCOMPATIBLE_TYPES.includes(block.slug)) {
|
|
3198
|
+
console.warn(`\u26A0\uFE0F Block "${block.slug}" may not be email-compatible. Consider creating an email-specific version.`);
|
|
3199
|
+
}
|
|
3200
|
+
const hasComplexFields = block.fields?.some((field) => {
|
|
3201
|
+
const complexTypes = ["code", "json", "richText", "blocks", "array"];
|
|
3202
|
+
return complexTypes.includes(field.type);
|
|
3203
|
+
});
|
|
3204
|
+
if (hasComplexFields) {
|
|
3205
|
+
console.warn(`\u26A0\uFE0F Block "${block.slug}" contains complex field types that may not render consistently in email clients.`);
|
|
3206
|
+
}
|
|
3207
|
+
});
|
|
3208
|
+
};
|
|
3209
|
+
var createEmailSafeBlocks = (customBlocks = []) => {
|
|
3210
|
+
validateEmailBlocks(customBlocks);
|
|
3211
|
+
const baseBlocks = [
|
|
3212
|
+
{
|
|
3213
|
+
slug: "button",
|
|
3214
|
+
fields: [
|
|
3215
|
+
{
|
|
3216
|
+
name: "text",
|
|
3217
|
+
type: "text",
|
|
3218
|
+
label: "Button Text",
|
|
3219
|
+
required: true
|
|
3220
|
+
},
|
|
3221
|
+
{
|
|
3222
|
+
name: "url",
|
|
3223
|
+
type: "text",
|
|
3224
|
+
label: "Button URL",
|
|
3225
|
+
required: true,
|
|
3226
|
+
admin: {
|
|
3227
|
+
description: "Enter the full URL (including https://)"
|
|
3228
|
+
}
|
|
3229
|
+
},
|
|
3230
|
+
{
|
|
3231
|
+
name: "style",
|
|
3232
|
+
type: "select",
|
|
3233
|
+
label: "Button Style",
|
|
3234
|
+
defaultValue: "primary",
|
|
3235
|
+
options: [
|
|
3236
|
+
{ label: "Primary", value: "primary" },
|
|
3237
|
+
{ label: "Secondary", value: "secondary" },
|
|
3238
|
+
{ label: "Outline", value: "outline" }
|
|
3239
|
+
]
|
|
3240
|
+
}
|
|
3241
|
+
],
|
|
3242
|
+
interfaceName: "EmailButton",
|
|
3243
|
+
labels: {
|
|
3244
|
+
singular: "Button",
|
|
3245
|
+
plural: "Buttons"
|
|
3246
|
+
}
|
|
3247
|
+
},
|
|
3248
|
+
{
|
|
3249
|
+
slug: "divider",
|
|
3250
|
+
fields: [
|
|
3251
|
+
{
|
|
3252
|
+
name: "style",
|
|
3253
|
+
type: "select",
|
|
3254
|
+
label: "Divider Style",
|
|
3255
|
+
defaultValue: "solid",
|
|
3256
|
+
options: [
|
|
3257
|
+
{ label: "Solid", value: "solid" },
|
|
3258
|
+
{ label: "Dashed", value: "dashed" },
|
|
3259
|
+
{ label: "Dotted", value: "dotted" }
|
|
3260
|
+
]
|
|
3261
|
+
}
|
|
3262
|
+
],
|
|
3263
|
+
interfaceName: "EmailDivider",
|
|
3264
|
+
labels: {
|
|
3265
|
+
singular: "Divider",
|
|
3266
|
+
plural: "Dividers"
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3269
|
+
];
|
|
3270
|
+
return [
|
|
3271
|
+
...baseBlocks,
|
|
3272
|
+
...customBlocks
|
|
3273
|
+
];
|
|
3274
|
+
};
|
|
3275
|
+
|
|
3276
|
+
// src/fields/emailContent.ts
|
|
3277
|
+
var createEmailSafeFeatures = (additionalBlocks) => {
|
|
3278
|
+
const baseBlocks = [
|
|
3279
|
+
{
|
|
3280
|
+
slug: "button",
|
|
3281
|
+
fields: [
|
|
3282
|
+
{
|
|
3283
|
+
name: "text",
|
|
3284
|
+
type: "text",
|
|
3285
|
+
label: "Button Text",
|
|
3286
|
+
required: true
|
|
3287
|
+
},
|
|
3288
|
+
{
|
|
3289
|
+
name: "url",
|
|
3290
|
+
type: "text",
|
|
3291
|
+
label: "Button URL",
|
|
3292
|
+
required: true,
|
|
3293
|
+
admin: {
|
|
3294
|
+
description: "Enter the full URL (including https://)"
|
|
3295
|
+
}
|
|
3296
|
+
},
|
|
3297
|
+
{
|
|
3298
|
+
name: "style",
|
|
3299
|
+
type: "select",
|
|
3300
|
+
label: "Button Style",
|
|
3301
|
+
defaultValue: "primary",
|
|
3302
|
+
options: [
|
|
3303
|
+
{ label: "Primary", value: "primary" },
|
|
3304
|
+
{ label: "Secondary", value: "secondary" },
|
|
3305
|
+
{ label: "Outline", value: "outline" }
|
|
3306
|
+
]
|
|
3307
|
+
}
|
|
3308
|
+
],
|
|
3309
|
+
interfaceName: "EmailButton",
|
|
3310
|
+
labels: {
|
|
3311
|
+
singular: "Button",
|
|
3312
|
+
plural: "Buttons"
|
|
3313
|
+
}
|
|
3314
|
+
},
|
|
3315
|
+
{
|
|
3316
|
+
slug: "divider",
|
|
3317
|
+
fields: [
|
|
3318
|
+
{
|
|
3319
|
+
name: "style",
|
|
3320
|
+
type: "select",
|
|
3321
|
+
label: "Divider Style",
|
|
3322
|
+
defaultValue: "solid",
|
|
3323
|
+
options: [
|
|
3324
|
+
{ label: "Solid", value: "solid" },
|
|
3325
|
+
{ label: "Dashed", value: "dashed" },
|
|
3326
|
+
{ label: "Dotted", value: "dotted" }
|
|
3327
|
+
]
|
|
3328
|
+
}
|
|
3329
|
+
],
|
|
3330
|
+
interfaceName: "EmailDivider",
|
|
3331
|
+
labels: {
|
|
3332
|
+
singular: "Divider",
|
|
3333
|
+
plural: "Dividers"
|
|
3334
|
+
}
|
|
3335
|
+
}
|
|
3336
|
+
];
|
|
3337
|
+
const allBlocks = [
|
|
3338
|
+
...baseBlocks,
|
|
3339
|
+
...additionalBlocks || []
|
|
3340
|
+
];
|
|
3341
|
+
return [
|
|
3342
|
+
// Toolbars
|
|
3343
|
+
(0, import_richtext_lexical.FixedToolbarFeature)(),
|
|
3344
|
+
// Fixed toolbar at the top
|
|
3345
|
+
(0, import_richtext_lexical.InlineToolbarFeature)(),
|
|
3346
|
+
// Floating toolbar when text is selected
|
|
3347
|
+
// Basic text formatting
|
|
3348
|
+
(0, import_richtext_lexical.BoldFeature)(),
|
|
3349
|
+
(0, import_richtext_lexical.ItalicFeature)(),
|
|
3350
|
+
(0, import_richtext_lexical.UnderlineFeature)(),
|
|
3351
|
+
(0, import_richtext_lexical.StrikethroughFeature)(),
|
|
3352
|
+
// Links with enhanced configuration
|
|
3353
|
+
(0, import_richtext_lexical.LinkFeature)({
|
|
3354
|
+
fields: [
|
|
3355
|
+
{
|
|
3356
|
+
name: "url",
|
|
3357
|
+
type: "text",
|
|
3358
|
+
required: true,
|
|
3359
|
+
admin: {
|
|
3360
|
+
description: "Enter the full URL (including https://)"
|
|
3361
|
+
}
|
|
3362
|
+
},
|
|
3363
|
+
{
|
|
3364
|
+
name: "newTab",
|
|
3365
|
+
type: "checkbox",
|
|
3366
|
+
label: "Open in new tab",
|
|
3367
|
+
defaultValue: false
|
|
3368
|
+
}
|
|
3369
|
+
]
|
|
3370
|
+
}),
|
|
3371
|
+
// Lists
|
|
3372
|
+
(0, import_richtext_lexical.OrderedListFeature)(),
|
|
3373
|
+
(0, import_richtext_lexical.UnorderedListFeature)(),
|
|
3374
|
+
// Headings - limited to h1, h2, h3 for email compatibility
|
|
3375
|
+
(0, import_richtext_lexical.HeadingFeature)({
|
|
3376
|
+
enabledHeadingSizes: ["h1", "h2", "h3"]
|
|
3377
|
+
}),
|
|
3378
|
+
// Basic paragraph and alignment
|
|
3379
|
+
(0, import_richtext_lexical.ParagraphFeature)(),
|
|
3380
|
+
(0, import_richtext_lexical.AlignFeature)(),
|
|
3381
|
+
// Blockquotes
|
|
3382
|
+
(0, import_richtext_lexical.BlockquoteFeature)(),
|
|
3383
|
+
// Upload feature for images
|
|
3384
|
+
(0, import_richtext_lexical.UploadFeature)({
|
|
3385
|
+
collections: {
|
|
3386
|
+
media: {
|
|
3387
|
+
fields: [
|
|
3388
|
+
{
|
|
3389
|
+
name: "caption",
|
|
3390
|
+
type: "text",
|
|
3391
|
+
admin: {
|
|
3392
|
+
description: "Optional caption for the image"
|
|
3393
|
+
}
|
|
3394
|
+
},
|
|
3395
|
+
{
|
|
3396
|
+
name: "altText",
|
|
3397
|
+
type: "text",
|
|
3398
|
+
label: "Alt Text",
|
|
3399
|
+
required: true,
|
|
3400
|
+
admin: {
|
|
3401
|
+
description: "Alternative text for accessibility and when image cannot be displayed"
|
|
3402
|
+
}
|
|
3403
|
+
}
|
|
3404
|
+
]
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
}),
|
|
3408
|
+
// Custom blocks for email-specific content
|
|
3409
|
+
(0, import_richtext_lexical.BlocksFeature)({
|
|
3410
|
+
blocks: allBlocks
|
|
3411
|
+
})
|
|
3412
|
+
];
|
|
3413
|
+
};
|
|
3414
|
+
var createEmailLexicalEditor = (customBlocks = []) => {
|
|
3415
|
+
const emailSafeBlocks = createEmailSafeBlocks(customBlocks);
|
|
3416
|
+
return (0, import_richtext_lexical.lexicalEditor)({
|
|
3417
|
+
features: [
|
|
3418
|
+
// Toolbars
|
|
3419
|
+
(0, import_richtext_lexical.FixedToolbarFeature)(),
|
|
3420
|
+
(0, import_richtext_lexical.InlineToolbarFeature)(),
|
|
3421
|
+
// Basic text formatting
|
|
3422
|
+
(0, import_richtext_lexical.BoldFeature)(),
|
|
3423
|
+
(0, import_richtext_lexical.ItalicFeature)(),
|
|
3424
|
+
(0, import_richtext_lexical.UnderlineFeature)(),
|
|
3425
|
+
(0, import_richtext_lexical.StrikethroughFeature)(),
|
|
3426
|
+
// Links with enhanced configuration
|
|
3427
|
+
(0, import_richtext_lexical.LinkFeature)({
|
|
3428
|
+
fields: [
|
|
3429
|
+
{
|
|
3430
|
+
name: "url",
|
|
3431
|
+
type: "text",
|
|
3432
|
+
required: true,
|
|
3433
|
+
admin: {
|
|
3434
|
+
description: "Enter the full URL (including https://)"
|
|
3435
|
+
}
|
|
3436
|
+
},
|
|
3437
|
+
{
|
|
3438
|
+
name: "newTab",
|
|
3439
|
+
type: "checkbox",
|
|
3440
|
+
label: "Open in new tab",
|
|
3441
|
+
defaultValue: false
|
|
3442
|
+
}
|
|
3443
|
+
]
|
|
3444
|
+
}),
|
|
3445
|
+
// Lists
|
|
3446
|
+
(0, import_richtext_lexical.OrderedListFeature)(),
|
|
3447
|
+
(0, import_richtext_lexical.UnorderedListFeature)(),
|
|
3448
|
+
// Headings - limited to h1, h2, h3 for email compatibility
|
|
3449
|
+
(0, import_richtext_lexical.HeadingFeature)({
|
|
3450
|
+
enabledHeadingSizes: ["h1", "h2", "h3"]
|
|
3451
|
+
}),
|
|
3452
|
+
// Basic paragraph and alignment
|
|
3453
|
+
(0, import_richtext_lexical.ParagraphFeature)(),
|
|
3454
|
+
(0, import_richtext_lexical.AlignFeature)(),
|
|
3455
|
+
// Blockquotes
|
|
3456
|
+
(0, import_richtext_lexical.BlockquoteFeature)(),
|
|
3457
|
+
// Upload feature for images
|
|
3458
|
+
(0, import_richtext_lexical.UploadFeature)({
|
|
3459
|
+
collections: {
|
|
3460
|
+
media: {
|
|
3461
|
+
fields: [
|
|
3462
|
+
{
|
|
3463
|
+
name: "caption",
|
|
3464
|
+
type: "text",
|
|
3465
|
+
admin: {
|
|
3466
|
+
description: "Optional caption for the image"
|
|
3467
|
+
}
|
|
3468
|
+
},
|
|
3469
|
+
{
|
|
3470
|
+
name: "altText",
|
|
3471
|
+
type: "text",
|
|
3472
|
+
label: "Alt Text",
|
|
3473
|
+
required: true,
|
|
3474
|
+
admin: {
|
|
3475
|
+
description: "Alternative text for accessibility and when image cannot be displayed"
|
|
3476
|
+
}
|
|
3477
|
+
}
|
|
3478
|
+
]
|
|
3479
|
+
}
|
|
3480
|
+
}
|
|
3481
|
+
}),
|
|
3482
|
+
// Email-safe blocks (processed server-side)
|
|
3483
|
+
(0, import_richtext_lexical.BlocksFeature)({
|
|
3484
|
+
blocks: emailSafeBlocks
|
|
3485
|
+
})
|
|
3486
|
+
]
|
|
3487
|
+
});
|
|
3488
|
+
};
|
|
3489
|
+
var emailSafeFeatures = createEmailSafeFeatures();
|
|
3490
|
+
var createEmailContentField = (overrides) => {
|
|
3491
|
+
const editor = overrides?.editor || createEmailLexicalEditor(overrides?.additionalBlocks);
|
|
3492
|
+
return {
|
|
3493
|
+
name: "content",
|
|
3494
|
+
type: "richText",
|
|
3495
|
+
required: true,
|
|
3496
|
+
editor,
|
|
3497
|
+
admin: {
|
|
3498
|
+
description: "Email content with limited formatting for compatibility",
|
|
3499
|
+
...overrides?.admin
|
|
3500
|
+
},
|
|
3501
|
+
...overrides
|
|
3502
|
+
};
|
|
3503
|
+
};
|
|
3504
|
+
|
|
3505
|
+
// src/fields/broadcastInlinePreview.ts
|
|
3506
|
+
var createBroadcastInlinePreviewField = () => {
|
|
3507
|
+
return {
|
|
3508
|
+
name: "broadcastInlinePreview",
|
|
3509
|
+
type: "ui",
|
|
3510
|
+
admin: {
|
|
3511
|
+
components: {
|
|
3512
|
+
Field: "payload-plugin-newsletter/components#BroadcastInlinePreview"
|
|
3513
|
+
}
|
|
3514
|
+
}
|
|
3515
|
+
};
|
|
3516
|
+
};
|
|
3517
|
+
|
|
3518
|
+
// src/utils/emailSafeHtml.ts
|
|
3519
|
+
var import_isomorphic_dompurify2 = __toESM(require("isomorphic-dompurify"), 1);
|
|
3520
|
+
var EMAIL_SAFE_CONFIG = {
|
|
3521
|
+
ALLOWED_TAGS: [
|
|
3522
|
+
"p",
|
|
3523
|
+
"br",
|
|
3524
|
+
"strong",
|
|
3525
|
+
"b",
|
|
3526
|
+
"em",
|
|
3527
|
+
"i",
|
|
3528
|
+
"u",
|
|
3529
|
+
"strike",
|
|
3140
3530
|
"s",
|
|
3141
3531
|
"span",
|
|
3142
3532
|
"a",
|
|
@@ -3403,819 +3793,420 @@ function getAlignment(format) {
|
|
|
3403
3793
|
function escapeHtml(text) {
|
|
3404
3794
|
const map = {
|
|
3405
3795
|
"&": "&",
|
|
3406
|
-
"<": "<",
|
|
3407
|
-
">": ">",
|
|
3408
|
-
'"': """,
|
|
3409
|
-
"'": "'"
|
|
3410
|
-
};
|
|
3411
|
-
return text.replace(/[&<>"']/g, (m) => map[m]);
|
|
3412
|
-
}
|
|
3413
|
-
function wrapInEmailTemplate(content, preheader) {
|
|
3414
|
-
return `<!DOCTYPE html>
|
|
3415
|
-
<html lang="en">
|
|
3416
|
-
<head>
|
|
3417
|
-
<meta charset="UTF-8">
|
|
3418
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
3419
|
-
<title>Email</title>
|
|
3420
|
-
<!--[if mso]>
|
|
3421
|
-
<noscript>
|
|
3422
|
-
<xml>
|
|
3423
|
-
<o:OfficeDocumentSettings>
|
|
3424
|
-
<o:PixelsPerInch>96</o:PixelsPerInch>
|
|
3425
|
-
</o:OfficeDocumentSettings>
|
|
3426
|
-
</xml>
|
|
3427
|
-
</noscript>
|
|
3428
|
-
<![endif]-->
|
|
3429
|
-
</head>
|
|
3430
|
-
<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: #333333; background-color: #f3f4f6;">
|
|
3431
|
-
${preheader ? `<div style="display: none; max-height: 0; overflow: hidden;">${escapeHtml(preheader)}</div>` : ""}
|
|
3432
|
-
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="margin: 0; padding: 0;">
|
|
3433
|
-
<tr>
|
|
3434
|
-
<td align="center" style="padding: 20px 0;">
|
|
3435
|
-
<table role="presentation" cellpadding="0" cellspacing="0" width="600" style="margin: 0 auto; background-color: #ffffff; border-radius: 8px; overflow: hidden;">
|
|
3436
|
-
<tr>
|
|
3437
|
-
<td style="padding: 40px 30px;">
|
|
3438
|
-
${content}
|
|
3439
|
-
</td>
|
|
3440
|
-
</tr>
|
|
3441
|
-
</table>
|
|
3442
|
-
</td>
|
|
3443
|
-
</tr>
|
|
3444
|
-
</table>
|
|
3445
|
-
</body>
|
|
3446
|
-
</html>`;
|
|
3447
|
-
}
|
|
3448
|
-
|
|
3449
|
-
// src/endpoints/broadcasts/test.ts
|
|
3450
|
-
var createTestBroadcastEndpoint = (config, collectionSlug) => {
|
|
3451
|
-
return {
|
|
3452
|
-
path: `/${collectionSlug}/:id/test`,
|
|
3453
|
-
method: "post",
|
|
3454
|
-
handler: async (req) => {
|
|
3455
|
-
try {
|
|
3456
|
-
const auth = await requireAdmin(req, config);
|
|
3457
|
-
if (!auth.authorized) {
|
|
3458
|
-
return Response.json({
|
|
3459
|
-
success: false,
|
|
3460
|
-
error: auth.error
|
|
3461
|
-
}, { status: 401 });
|
|
3462
|
-
}
|
|
3463
|
-
const url = new URL(req.url || "", `http://localhost`);
|
|
3464
|
-
const pathParts = url.pathname.split("/");
|
|
3465
|
-
const id = pathParts[pathParts.length - 2];
|
|
3466
|
-
if (!id) {
|
|
3467
|
-
return Response.json({
|
|
3468
|
-
success: false,
|
|
3469
|
-
error: "Broadcast ID is required"
|
|
3470
|
-
}, { status: 400 });
|
|
3471
|
-
}
|
|
3472
|
-
const data = await (req.json?.() || Promise.resolve({}));
|
|
3473
|
-
const testEmail = data.email || auth.user.email;
|
|
3474
|
-
if (!testEmail) {
|
|
3475
|
-
return Response.json({
|
|
3476
|
-
success: false,
|
|
3477
|
-
error: "No email address available for test send"
|
|
3478
|
-
}, { status: 400 });
|
|
3479
|
-
}
|
|
3480
|
-
const broadcast = await req.payload.findByID({
|
|
3481
|
-
collection: collectionSlug,
|
|
3482
|
-
id,
|
|
3483
|
-
user: auth.user
|
|
3484
|
-
});
|
|
3485
|
-
if (!broadcast) {
|
|
3486
|
-
return Response.json({
|
|
3487
|
-
success: false,
|
|
3488
|
-
error: "Broadcast not found"
|
|
3489
|
-
}, { status: 404 });
|
|
3490
|
-
}
|
|
3491
|
-
const htmlContent = await convertToEmailSafeHtml(broadcast.content, {
|
|
3492
|
-
wrapInTemplate: true,
|
|
3493
|
-
preheader: broadcast.preheader,
|
|
3494
|
-
customBlockConverter: config.customizations?.broadcasts?.customBlockConverter
|
|
3495
|
-
});
|
|
3496
|
-
const emailService = req.payload.newsletterEmailService;
|
|
3497
|
-
if (!emailService) {
|
|
3498
|
-
return Response.json({
|
|
3499
|
-
success: false,
|
|
3500
|
-
error: "Email service is not configured"
|
|
3501
|
-
}, { status: 500 });
|
|
3502
|
-
}
|
|
3503
|
-
const providerConfig = config.providers.default === "resend" ? config.providers.resend : config.providers.broadcast;
|
|
3504
|
-
const fromEmail = providerConfig?.fromAddress || providerConfig?.fromEmail || "noreply@example.com";
|
|
3505
|
-
const fromName = providerConfig?.fromName || "Newsletter";
|
|
3506
|
-
const replyTo = broadcast.settings?.replyTo || providerConfig?.replyTo;
|
|
3507
|
-
await emailService.send({
|
|
3508
|
-
to: testEmail,
|
|
3509
|
-
from: fromEmail,
|
|
3510
|
-
fromName,
|
|
3511
|
-
replyTo,
|
|
3512
|
-
subject: `[TEST] ${broadcast.subject}`,
|
|
3513
|
-
html: htmlContent,
|
|
3514
|
-
trackOpens: false,
|
|
3515
|
-
trackClicks: false
|
|
3516
|
-
});
|
|
3517
|
-
return Response.json({
|
|
3518
|
-
success: true,
|
|
3519
|
-
message: `Test email sent to ${testEmail}`
|
|
3520
|
-
});
|
|
3521
|
-
} catch (error) {
|
|
3522
|
-
console.error("Failed to send test broadcast:", error);
|
|
3523
|
-
return Response.json({
|
|
3524
|
-
success: false,
|
|
3525
|
-
error: "Failed to send test email"
|
|
3526
|
-
}, { status: 500 });
|
|
3527
|
-
}
|
|
3528
|
-
}
|
|
3796
|
+
"<": "<",
|
|
3797
|
+
">": ">",
|
|
3798
|
+
'"': """,
|
|
3799
|
+
"'": "'"
|
|
3529
3800
|
};
|
|
3530
|
-
|
|
3801
|
+
return text.replace(/[&<>"']/g, (m) => map[m]);
|
|
3802
|
+
}
|
|
3803
|
+
function wrapInEmailTemplate(content, preheader) {
|
|
3804
|
+
return `<!DOCTYPE html>
|
|
3805
|
+
<html lang="en">
|
|
3806
|
+
<head>
|
|
3807
|
+
<meta charset="UTF-8">
|
|
3808
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
3809
|
+
<title>Email</title>
|
|
3810
|
+
<!--[if mso]>
|
|
3811
|
+
<noscript>
|
|
3812
|
+
<xml>
|
|
3813
|
+
<o:OfficeDocumentSettings>
|
|
3814
|
+
<o:PixelsPerInch>96</o:PixelsPerInch>
|
|
3815
|
+
</o:OfficeDocumentSettings>
|
|
3816
|
+
</xml>
|
|
3817
|
+
</noscript>
|
|
3818
|
+
<![endif]-->
|
|
3819
|
+
</head>
|
|
3820
|
+
<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: #333333; background-color: #f3f4f6;">
|
|
3821
|
+
${preheader ? `<div style="display: none; max-height: 0; overflow: hidden;">${escapeHtml(preheader)}</div>` : ""}
|
|
3822
|
+
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="margin: 0; padding: 0;">
|
|
3823
|
+
<tr>
|
|
3824
|
+
<td align="center" style="padding: 20px 0;">
|
|
3825
|
+
<table role="presentation" cellpadding="0" cellspacing="0" width="600" style="margin: 0 auto; background-color: #ffffff; border-radius: 8px; overflow: hidden;">
|
|
3826
|
+
<tr>
|
|
3827
|
+
<td style="padding: 40px 30px;">
|
|
3828
|
+
${content}
|
|
3829
|
+
</td>
|
|
3830
|
+
</tr>
|
|
3831
|
+
</table>
|
|
3832
|
+
</td>
|
|
3833
|
+
</tr>
|
|
3834
|
+
</table>
|
|
3835
|
+
</body>
|
|
3836
|
+
</html>`;
|
|
3837
|
+
}
|
|
3531
3838
|
|
|
3532
|
-
// src/
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
const mediaUrl = req.payload.config.serverURL ? `${req.payload.config.serverURL}/api/media` : "/api/media";
|
|
3548
|
-
const htmlContent = await convertToEmailSafeHtml(content, {
|
|
3549
|
-
wrapInTemplate: true,
|
|
3550
|
-
preheader,
|
|
3551
|
-
mediaUrl,
|
|
3552
|
-
customBlockConverter: config.customizations?.broadcasts?.customBlockConverter
|
|
3553
|
-
});
|
|
3554
|
-
return Response.json({
|
|
3555
|
-
success: true,
|
|
3556
|
-
preview: {
|
|
3557
|
-
subject: subject || "Preview",
|
|
3558
|
-
preheader: preheader || "",
|
|
3559
|
-
html: htmlContent
|
|
3560
|
-
}
|
|
3561
|
-
});
|
|
3562
|
-
} catch (error) {
|
|
3563
|
-
console.error("Failed to generate email preview:", error);
|
|
3564
|
-
return Response.json({
|
|
3565
|
-
success: false,
|
|
3566
|
-
error: "Failed to generate email preview"
|
|
3567
|
-
}, { status: 500 });
|
|
3568
|
-
}
|
|
3839
|
+
// src/utils/getBroadcastConfig.ts
|
|
3840
|
+
async function getBroadcastConfig(req, pluginConfig) {
|
|
3841
|
+
try {
|
|
3842
|
+
const settings = await req.payload.findGlobal({
|
|
3843
|
+
slug: pluginConfig.settingsSlug || "newsletter-settings",
|
|
3844
|
+
req
|
|
3845
|
+
});
|
|
3846
|
+
if (settings?.provider === "broadcast" && settings?.broadcastSettings) {
|
|
3847
|
+
return {
|
|
3848
|
+
apiUrl: settings.broadcastSettings.apiUrl || pluginConfig.providers?.broadcast?.apiUrl || "",
|
|
3849
|
+
token: settings.broadcastSettings.token || pluginConfig.providers?.broadcast?.token || "",
|
|
3850
|
+
fromAddress: settings.fromAddress || pluginConfig.providers?.broadcast?.fromAddress || "",
|
|
3851
|
+
fromName: settings.fromName || pluginConfig.providers?.broadcast?.fromName || "",
|
|
3852
|
+
replyTo: settings.replyTo || pluginConfig.providers?.broadcast?.replyTo
|
|
3853
|
+
};
|
|
3569
3854
|
}
|
|
3570
|
-
|
|
3571
|
-
}
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
var createBroadcastManagementEndpoints = (config) => {
|
|
3575
|
-
if (!config.features?.newsletterManagement?.enabled) {
|
|
3576
|
-
return [];
|
|
3577
|
-
}
|
|
3578
|
-
const collectionSlug = config.features.newsletterManagement.collections?.broadcasts || "broadcasts";
|
|
3579
|
-
return [
|
|
3580
|
-
createSendBroadcastEndpoint(config, collectionSlug),
|
|
3581
|
-
createScheduleBroadcastEndpoint(config, collectionSlug),
|
|
3582
|
-
createTestBroadcastEndpoint(config, collectionSlug),
|
|
3583
|
-
createBroadcastPreviewEndpoint(config, collectionSlug)
|
|
3584
|
-
];
|
|
3585
|
-
};
|
|
3586
|
-
|
|
3587
|
-
// src/endpoints/index.ts
|
|
3588
|
-
function createNewsletterEndpoints(config) {
|
|
3589
|
-
const endpoints = [
|
|
3590
|
-
createSubscribeEndpoint(config),
|
|
3591
|
-
createUnsubscribeEndpoint(config)
|
|
3592
|
-
];
|
|
3593
|
-
if (config.auth?.enabled !== false) {
|
|
3594
|
-
endpoints.push(
|
|
3595
|
-
createVerifyMagicLinkEndpoint(config),
|
|
3596
|
-
createPreferencesEndpoint(config),
|
|
3597
|
-
createUpdatePreferencesEndpoint(config),
|
|
3598
|
-
createSigninEndpoint(config),
|
|
3599
|
-
createMeEndpoint(config),
|
|
3600
|
-
createSignoutEndpoint(config)
|
|
3601
|
-
);
|
|
3855
|
+
return pluginConfig.providers?.broadcast || null;
|
|
3856
|
+
} catch (error) {
|
|
3857
|
+
req.payload.logger.error("Failed to get broadcast config from settings:", error);
|
|
3858
|
+
return pluginConfig.providers?.broadcast || null;
|
|
3602
3859
|
}
|
|
3603
|
-
endpoints.push(...createBroadcastManagementEndpoints(config));
|
|
3604
|
-
return endpoints;
|
|
3605
3860
|
}
|
|
3606
3861
|
|
|
3607
|
-
// src/
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
{
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
},
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
admin: {
|
|
3628
|
-
description: "Schedule this content to be sent as a newsletter"
|
|
3629
|
-
}
|
|
3630
|
-
},
|
|
3631
|
-
{
|
|
3632
|
-
name: "scheduledDate",
|
|
3633
|
-
type: "date",
|
|
3634
|
-
label: "Send Date",
|
|
3635
|
-
required: true,
|
|
3636
|
-
admin: {
|
|
3637
|
-
date: {
|
|
3638
|
-
pickerAppearance: "dayAndTime"
|
|
3639
|
-
},
|
|
3640
|
-
condition: (data) => data?.[groupName]?.scheduled,
|
|
3641
|
-
description: "When to send this newsletter"
|
|
3642
|
-
}
|
|
3643
|
-
},
|
|
3644
|
-
{
|
|
3645
|
-
name: "sentDate",
|
|
3646
|
-
type: "date",
|
|
3647
|
-
label: "Sent Date",
|
|
3648
|
-
admin: {
|
|
3649
|
-
readOnly: true,
|
|
3650
|
-
condition: (data) => data?.[groupName]?.sendStatus === "sent",
|
|
3651
|
-
description: "When this newsletter was sent"
|
|
3652
|
-
}
|
|
3653
|
-
},
|
|
3654
|
-
{
|
|
3655
|
-
name: "sendStatus",
|
|
3656
|
-
type: "select",
|
|
3657
|
-
label: "Status",
|
|
3658
|
-
options: [
|
|
3659
|
-
{ label: "Draft", value: "draft" },
|
|
3660
|
-
{ label: "Scheduled", value: "scheduled" },
|
|
3661
|
-
{ label: "Sending", value: "sending" },
|
|
3662
|
-
{ label: "Sent", value: "sent" },
|
|
3663
|
-
{ label: "Failed", value: "failed" }
|
|
3664
|
-
],
|
|
3665
|
-
defaultValue: "draft",
|
|
3666
|
-
admin: {
|
|
3667
|
-
readOnly: true,
|
|
3668
|
-
description: "Current send status"
|
|
3669
|
-
}
|
|
3670
|
-
},
|
|
3671
|
-
{
|
|
3672
|
-
name: "emailSubject",
|
|
3673
|
-
type: "text",
|
|
3674
|
-
label: "Email Subject",
|
|
3675
|
-
required: true,
|
|
3676
|
-
admin: {
|
|
3677
|
-
condition: (data) => data?.[groupName]?.scheduled,
|
|
3678
|
-
description: "Subject line for the newsletter email"
|
|
3679
|
-
}
|
|
3680
|
-
},
|
|
3681
|
-
{
|
|
3682
|
-
name: "preheader",
|
|
3683
|
-
type: "text",
|
|
3684
|
-
label: "Email Preheader",
|
|
3685
|
-
admin: {
|
|
3686
|
-
condition: (data) => data?.[groupName]?.scheduled,
|
|
3687
|
-
description: "Preview text that appears after the subject line"
|
|
3688
|
-
}
|
|
3689
|
-
},
|
|
3690
|
-
{
|
|
3691
|
-
name: "segments",
|
|
3692
|
-
type: "select",
|
|
3693
|
-
label: "Target Segments",
|
|
3694
|
-
hasMany: true,
|
|
3695
|
-
options: [
|
|
3696
|
-
{ label: "All Subscribers", value: "all" },
|
|
3697
|
-
...config.i18n?.locales?.map((locale) => ({
|
|
3698
|
-
label: `${locale.toUpperCase()} Subscribers`,
|
|
3699
|
-
value: locale
|
|
3700
|
-
})) || []
|
|
3701
|
-
],
|
|
3702
|
-
defaultValue: ["all"],
|
|
3703
|
-
admin: {
|
|
3704
|
-
condition: (data) => data?.[groupName]?.scheduled,
|
|
3705
|
-
description: "Which subscriber segments to send to"
|
|
3706
|
-
}
|
|
3707
|
-
},
|
|
3708
|
-
{
|
|
3709
|
-
name: "testEmails",
|
|
3710
|
-
type: "array",
|
|
3711
|
-
label: "Test Email Recipients",
|
|
3712
|
-
admin: {
|
|
3713
|
-
condition: (data) => data?.[groupName]?.scheduled && data?.[groupName]?.sendStatus === "draft",
|
|
3714
|
-
description: "Send test emails before scheduling"
|
|
3715
|
-
},
|
|
3716
|
-
fields: [
|
|
3717
|
-
{
|
|
3718
|
-
name: "email",
|
|
3719
|
-
type: "email",
|
|
3720
|
-
required: true
|
|
3721
|
-
}
|
|
3722
|
-
]
|
|
3723
|
-
}
|
|
3724
|
-
]
|
|
3725
|
-
}
|
|
3726
|
-
];
|
|
3727
|
-
if (createMarkdownField) {
|
|
3728
|
-
fields.push(createMarkdownFieldInternal({
|
|
3729
|
-
name: `${contentField}Markdown`,
|
|
3730
|
-
richTextField: contentField,
|
|
3731
|
-
label: "Email Content (Markdown)",
|
|
3732
|
-
admin: {
|
|
3733
|
-
position: "sidebar",
|
|
3734
|
-
condition: (data) => Boolean(data?.[contentField] && data?.[groupName]?.scheduled),
|
|
3735
|
-
description: "Markdown version for email rendering",
|
|
3736
|
-
readOnly: true
|
|
3737
|
-
}
|
|
3738
|
-
}));
|
|
3862
|
+
// src/endpoints/broadcasts/send.ts
|
|
3863
|
+
init_types();
|
|
3864
|
+
|
|
3865
|
+
// src/utils/auth.ts
|
|
3866
|
+
async function getAuthenticatedUser(req) {
|
|
3867
|
+
try {
|
|
3868
|
+
const me = await req.payload.find({
|
|
3869
|
+
collection: "users",
|
|
3870
|
+
where: {
|
|
3871
|
+
id: {
|
|
3872
|
+
equals: "me"
|
|
3873
|
+
// Special value in Payload to get current user
|
|
3874
|
+
}
|
|
3875
|
+
},
|
|
3876
|
+
limit: 1,
|
|
3877
|
+
depth: 0
|
|
3878
|
+
});
|
|
3879
|
+
return me.docs[0] || null;
|
|
3880
|
+
} catch {
|
|
3881
|
+
return null;
|
|
3739
3882
|
}
|
|
3740
|
-
return fields;
|
|
3741
3883
|
}
|
|
3742
|
-
function
|
|
3884
|
+
async function requireAdmin(req, config) {
|
|
3885
|
+
const user = await getAuthenticatedUser(req);
|
|
3886
|
+
if (!user) {
|
|
3887
|
+
return {
|
|
3888
|
+
authorized: false,
|
|
3889
|
+
error: "Authentication required"
|
|
3890
|
+
};
|
|
3891
|
+
}
|
|
3892
|
+
if (!isAdmin(user, config)) {
|
|
3893
|
+
return {
|
|
3894
|
+
authorized: false,
|
|
3895
|
+
error: "Admin access required"
|
|
3896
|
+
};
|
|
3897
|
+
}
|
|
3743
3898
|
return {
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
label: config.label || "Markdown",
|
|
3747
|
-
admin: {
|
|
3748
|
-
...config.admin,
|
|
3749
|
-
description: config.admin?.description || "Auto-generated from rich text content"
|
|
3750
|
-
},
|
|
3751
|
-
hooks: {
|
|
3752
|
-
afterRead: [
|
|
3753
|
-
async ({ data }) => {
|
|
3754
|
-
if (data?.[config.richTextField]) {
|
|
3755
|
-
try {
|
|
3756
|
-
const { convertLexicalToMarkdown } = await import("@payloadcms/richtext-lexical");
|
|
3757
|
-
return convertLexicalToMarkdown({
|
|
3758
|
-
data: data[config.richTextField]
|
|
3759
|
-
});
|
|
3760
|
-
} catch {
|
|
3761
|
-
return "";
|
|
3762
|
-
}
|
|
3763
|
-
}
|
|
3764
|
-
return "";
|
|
3765
|
-
}
|
|
3766
|
-
],
|
|
3767
|
-
beforeChange: [
|
|
3768
|
-
() => {
|
|
3769
|
-
return null;
|
|
3770
|
-
}
|
|
3771
|
-
]
|
|
3772
|
-
}
|
|
3899
|
+
authorized: true,
|
|
3900
|
+
user
|
|
3773
3901
|
};
|
|
3774
3902
|
}
|
|
3775
3903
|
|
|
3776
|
-
// src/
|
|
3777
|
-
var
|
|
3904
|
+
// src/endpoints/broadcasts/send.ts
|
|
3905
|
+
var createSendBroadcastEndpoint = (config, collectionSlug) => {
|
|
3778
3906
|
return {
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
handler: async (
|
|
3782
|
-
const subscribersSlug = pluginConfig.subscribersSlug || "subscribers";
|
|
3783
|
-
const emailService = req.payload.newsletterEmailService;
|
|
3784
|
-
if (!emailService) {
|
|
3785
|
-
console.error("Email service not configured");
|
|
3786
|
-
return {
|
|
3787
|
-
output: {
|
|
3788
|
-
syncedCount: 0
|
|
3789
|
-
}
|
|
3790
|
-
};
|
|
3791
|
-
}
|
|
3792
|
-
let syncedCount = 0;
|
|
3907
|
+
path: "/:id/send",
|
|
3908
|
+
method: "post",
|
|
3909
|
+
handler: async (req) => {
|
|
3793
3910
|
try {
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
}
|
|
3800
|
-
const apiUrl = broadcastConfig.apiUrl.replace(/\/$/, "");
|
|
3801
|
-
const token = broadcastConfig.token;
|
|
3802
|
-
let page = 1;
|
|
3803
|
-
let hasMore = true;
|
|
3804
|
-
while (hasMore) {
|
|
3805
|
-
const response = await fetch(
|
|
3806
|
-
`${apiUrl}/api/v1/subscribers.json?page=${page}`,
|
|
3807
|
-
{
|
|
3808
|
-
headers: {
|
|
3809
|
-
"Authorization": `Bearer ${token}`
|
|
3810
|
-
}
|
|
3811
|
-
}
|
|
3812
|
-
);
|
|
3813
|
-
if (!response.ok) {
|
|
3814
|
-
throw new Error(`Broadcast API error: ${response.status}`);
|
|
3815
|
-
}
|
|
3816
|
-
const data = await response.json();
|
|
3817
|
-
const broadcastSubscribers = data.subscribers || [];
|
|
3818
|
-
for (const broadcastSub of broadcastSubscribers) {
|
|
3819
|
-
const payloadSubscribers = await req.payload.find({
|
|
3820
|
-
collection: subscribersSlug,
|
|
3821
|
-
where: {
|
|
3822
|
-
email: {
|
|
3823
|
-
equals: broadcastSub.email
|
|
3824
|
-
}
|
|
3825
|
-
},
|
|
3826
|
-
limit: 1
|
|
3827
|
-
});
|
|
3828
|
-
if (payloadSubscribers.docs.length > 0) {
|
|
3829
|
-
const payloadSub = payloadSubscribers.docs[0];
|
|
3830
|
-
const broadcastUnsubscribed = !broadcastSub.is_active || broadcastSub.unsubscribed_at;
|
|
3831
|
-
const payloadUnsubscribed = payloadSub.subscriptionStatus === "unsubscribed";
|
|
3832
|
-
if (broadcastUnsubscribed && !payloadUnsubscribed) {
|
|
3833
|
-
await req.payload.update({
|
|
3834
|
-
collection: subscribersSlug,
|
|
3835
|
-
id: payloadSub.id,
|
|
3836
|
-
data: {
|
|
3837
|
-
subscriptionStatus: "unsubscribed",
|
|
3838
|
-
unsubscribedAt: broadcastSub.unsubscribed_at || (/* @__PURE__ */ new Date()).toISOString()
|
|
3839
|
-
}
|
|
3840
|
-
});
|
|
3841
|
-
syncedCount++;
|
|
3842
|
-
console.warn(`Unsubscribed: ${broadcastSub.email}`);
|
|
3843
|
-
}
|
|
3844
|
-
}
|
|
3845
|
-
}
|
|
3846
|
-
if (data.pagination && data.pagination.current < data.pagination.total_pages) {
|
|
3847
|
-
page++;
|
|
3848
|
-
} else {
|
|
3849
|
-
hasMore = false;
|
|
3850
|
-
}
|
|
3851
|
-
}
|
|
3852
|
-
console.warn(`Broadcast sync complete. Unsubscribed ${syncedCount} contacts.`);
|
|
3853
|
-
}
|
|
3854
|
-
if (emailService.getProvider() === "resend") {
|
|
3855
|
-
console.warn("Starting Resend unsubscribe sync...");
|
|
3856
|
-
const resendConfig = pluginConfig.providers?.resend;
|
|
3857
|
-
if (!resendConfig) {
|
|
3858
|
-
throw new Error("Resend configuration not found");
|
|
3859
|
-
}
|
|
3860
|
-
console.warn("Resend polling implementation needed - webhooks recommended");
|
|
3911
|
+
const auth = await requireAdmin(req, config);
|
|
3912
|
+
if (!auth.authorized) {
|
|
3913
|
+
return Response.json({
|
|
3914
|
+
success: false,
|
|
3915
|
+
error: auth.error
|
|
3916
|
+
}, { status: 401 });
|
|
3861
3917
|
}
|
|
3862
|
-
if (
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
});
|
|
3918
|
+
if (!config.features?.newsletterManagement?.enabled) {
|
|
3919
|
+
return Response.json({
|
|
3920
|
+
success: false,
|
|
3921
|
+
error: "Broadcast management is not enabled"
|
|
3922
|
+
}, { status: 400 });
|
|
3867
3923
|
}
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3924
|
+
const url = new URL(req.url || "", `http://localhost`);
|
|
3925
|
+
const pathParts = url.pathname.split("/");
|
|
3926
|
+
const id = pathParts[pathParts.length - 2];
|
|
3927
|
+
if (!id) {
|
|
3928
|
+
return Response.json({
|
|
3929
|
+
success: false,
|
|
3930
|
+
error: "Broadcast ID is required"
|
|
3931
|
+
}, { status: 400 });
|
|
3875
3932
|
}
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
// src/utils/blockValidation.ts
|
|
3888
|
-
var EMAIL_INCOMPATIBLE_TYPES = [
|
|
3889
|
-
"chart",
|
|
3890
|
-
"dataTable",
|
|
3891
|
-
"interactive",
|
|
3892
|
-
"streamable",
|
|
3893
|
-
"video",
|
|
3894
|
-
"iframe",
|
|
3895
|
-
"form",
|
|
3896
|
-
"carousel",
|
|
3897
|
-
"tabs",
|
|
3898
|
-
"accordion",
|
|
3899
|
-
"map"
|
|
3900
|
-
];
|
|
3901
|
-
var validateEmailBlocks = (blocks) => {
|
|
3902
|
-
blocks.forEach((block) => {
|
|
3903
|
-
if (EMAIL_INCOMPATIBLE_TYPES.includes(block.slug)) {
|
|
3904
|
-
console.warn(`\u26A0\uFE0F Block "${block.slug}" may not be email-compatible. Consider creating an email-specific version.`);
|
|
3905
|
-
}
|
|
3906
|
-
const hasComplexFields = block.fields?.some((field) => {
|
|
3907
|
-
const complexTypes = ["code", "json", "richText", "blocks", "array"];
|
|
3908
|
-
return complexTypes.includes(field.type);
|
|
3909
|
-
});
|
|
3910
|
-
if (hasComplexFields) {
|
|
3911
|
-
console.warn(`\u26A0\uFE0F Block "${block.slug}" contains complex field types that may not render consistently in email clients.`);
|
|
3912
|
-
}
|
|
3913
|
-
});
|
|
3914
|
-
};
|
|
3915
|
-
var createEmailSafeBlocks = (customBlocks = []) => {
|
|
3916
|
-
validateEmailBlocks(customBlocks);
|
|
3917
|
-
const baseBlocks = [
|
|
3918
|
-
{
|
|
3919
|
-
slug: "button",
|
|
3920
|
-
fields: [
|
|
3921
|
-
{
|
|
3922
|
-
name: "text",
|
|
3923
|
-
type: "text",
|
|
3924
|
-
label: "Button Text",
|
|
3925
|
-
required: true
|
|
3926
|
-
},
|
|
3927
|
-
{
|
|
3928
|
-
name: "url",
|
|
3929
|
-
type: "text",
|
|
3930
|
-
label: "Button URL",
|
|
3931
|
-
required: true,
|
|
3932
|
-
admin: {
|
|
3933
|
-
description: "Enter the full URL (including https://)"
|
|
3934
|
-
}
|
|
3935
|
-
},
|
|
3936
|
-
{
|
|
3937
|
-
name: "style",
|
|
3938
|
-
type: "select",
|
|
3939
|
-
label: "Button Style",
|
|
3940
|
-
defaultValue: "primary",
|
|
3941
|
-
options: [
|
|
3942
|
-
{ label: "Primary", value: "primary" },
|
|
3943
|
-
{ label: "Secondary", value: "secondary" },
|
|
3944
|
-
{ label: "Outline", value: "outline" }
|
|
3945
|
-
]
|
|
3933
|
+
const data = await (req.json?.() || Promise.resolve({}));
|
|
3934
|
+
const broadcastDoc = await req.payload.findByID({
|
|
3935
|
+
collection: collectionSlug,
|
|
3936
|
+
id,
|
|
3937
|
+
user: auth.user
|
|
3938
|
+
});
|
|
3939
|
+
if (!broadcastDoc || !broadcastDoc.providerId) {
|
|
3940
|
+
return Response.json({
|
|
3941
|
+
success: false,
|
|
3942
|
+
error: "Broadcast not found or not synced with provider"
|
|
3943
|
+
}, { status: 404 });
|
|
3946
3944
|
}
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
},
|
|
3954
|
-
{
|
|
3955
|
-
slug: "divider",
|
|
3956
|
-
fields: [
|
|
3957
|
-
{
|
|
3958
|
-
name: "style",
|
|
3959
|
-
type: "select",
|
|
3960
|
-
label: "Divider Style",
|
|
3961
|
-
defaultValue: "solid",
|
|
3962
|
-
options: [
|
|
3963
|
-
{ label: "Solid", value: "solid" },
|
|
3964
|
-
{ label: "Dashed", value: "dashed" },
|
|
3965
|
-
{ label: "Dotted", value: "dotted" }
|
|
3966
|
-
]
|
|
3945
|
+
const providerConfig = await getBroadcastConfig(req, config);
|
|
3946
|
+
if (!providerConfig || !providerConfig.token) {
|
|
3947
|
+
return Response.json({
|
|
3948
|
+
success: false,
|
|
3949
|
+
error: "Broadcast provider not configured in Newsletter Settings or environment variables"
|
|
3950
|
+
}, { status: 500 });
|
|
3967
3951
|
}
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3952
|
+
const { BroadcastApiProvider: BroadcastApiProvider2 } = await Promise.resolve().then(() => (init_broadcast2(), broadcast_exports));
|
|
3953
|
+
const provider = new BroadcastApiProvider2(providerConfig);
|
|
3954
|
+
const broadcast = await provider.send(broadcastDoc.providerId, data);
|
|
3955
|
+
await req.payload.update({
|
|
3956
|
+
collection: collectionSlug,
|
|
3957
|
+
id,
|
|
3958
|
+
data: {
|
|
3959
|
+
sendStatus: "sending" /* SENDING */,
|
|
3960
|
+
sentAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3961
|
+
},
|
|
3962
|
+
user: auth.user
|
|
3963
|
+
});
|
|
3964
|
+
return Response.json({
|
|
3965
|
+
success: true,
|
|
3966
|
+
message: "Broadcast sent successfully",
|
|
3967
|
+
broadcast
|
|
3968
|
+
});
|
|
3969
|
+
} catch (error) {
|
|
3970
|
+
console.error("Failed to send broadcast:", error);
|
|
3971
|
+
if (error instanceof NewsletterProviderError) {
|
|
3972
|
+
return Response.json({
|
|
3973
|
+
success: false,
|
|
3974
|
+
error: error.message,
|
|
3975
|
+
code: error.code
|
|
3976
|
+
}, { status: error.code === "NOT_SUPPORTED" ? 501 : 500 });
|
|
3977
|
+
}
|
|
3978
|
+
return Response.json({
|
|
3979
|
+
success: false,
|
|
3980
|
+
error: "Failed to send broadcast"
|
|
3981
|
+
}, { status: 500 });
|
|
3973
3982
|
}
|
|
3974
3983
|
}
|
|
3975
|
-
|
|
3976
|
-
return [
|
|
3977
|
-
...baseBlocks,
|
|
3978
|
-
...customBlocks
|
|
3979
|
-
];
|
|
3984
|
+
};
|
|
3980
3985
|
};
|
|
3981
3986
|
|
|
3982
|
-
// src/
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
type: "text",
|
|
3997
|
-
label: "Button URL",
|
|
3998
|
-
required: true,
|
|
3999
|
-
admin: {
|
|
4000
|
-
description: "Enter the full URL (including https://)"
|
|
4001
|
-
}
|
|
4002
|
-
},
|
|
4003
|
-
{
|
|
4004
|
-
name: "style",
|
|
4005
|
-
type: "select",
|
|
4006
|
-
label: "Button Style",
|
|
4007
|
-
defaultValue: "primary",
|
|
4008
|
-
options: [
|
|
4009
|
-
{ label: "Primary", value: "primary" },
|
|
4010
|
-
{ label: "Secondary", value: "secondary" },
|
|
4011
|
-
{ label: "Outline", value: "outline" }
|
|
4012
|
-
]
|
|
3987
|
+
// src/endpoints/broadcasts/schedule.ts
|
|
3988
|
+
init_types();
|
|
3989
|
+
var createScheduleBroadcastEndpoint = (config, collectionSlug) => {
|
|
3990
|
+
return {
|
|
3991
|
+
path: "/:id/schedule",
|
|
3992
|
+
method: "post",
|
|
3993
|
+
handler: async (req) => {
|
|
3994
|
+
try {
|
|
3995
|
+
const auth = await requireAdmin(req, config);
|
|
3996
|
+
if (!auth.authorized) {
|
|
3997
|
+
return Response.json({
|
|
3998
|
+
success: false,
|
|
3999
|
+
error: auth.error
|
|
4000
|
+
}, { status: 401 });
|
|
4013
4001
|
}
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
}
|
|
4020
|
-
},
|
|
4021
|
-
{
|
|
4022
|
-
slug: "divider",
|
|
4023
|
-
fields: [
|
|
4024
|
-
{
|
|
4025
|
-
name: "style",
|
|
4026
|
-
type: "select",
|
|
4027
|
-
label: "Divider Style",
|
|
4028
|
-
defaultValue: "solid",
|
|
4029
|
-
options: [
|
|
4030
|
-
{ label: "Solid", value: "solid" },
|
|
4031
|
-
{ label: "Dashed", value: "dashed" },
|
|
4032
|
-
{ label: "Dotted", value: "dotted" }
|
|
4033
|
-
]
|
|
4002
|
+
if (!config.features?.newsletterManagement?.enabled) {
|
|
4003
|
+
return Response.json({
|
|
4004
|
+
success: false,
|
|
4005
|
+
error: "Broadcast management is not enabled"
|
|
4006
|
+
}, { status: 400 });
|
|
4034
4007
|
}
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
const allBlocks = [
|
|
4044
|
-
...baseBlocks,
|
|
4045
|
-
...additionalBlocks || []
|
|
4046
|
-
];
|
|
4047
|
-
return [
|
|
4048
|
-
// Toolbars
|
|
4049
|
-
(0, import_richtext_lexical.FixedToolbarFeature)(),
|
|
4050
|
-
// Fixed toolbar at the top
|
|
4051
|
-
(0, import_richtext_lexical.InlineToolbarFeature)(),
|
|
4052
|
-
// Floating toolbar when text is selected
|
|
4053
|
-
// Basic text formatting
|
|
4054
|
-
(0, import_richtext_lexical.BoldFeature)(),
|
|
4055
|
-
(0, import_richtext_lexical.ItalicFeature)(),
|
|
4056
|
-
(0, import_richtext_lexical.UnderlineFeature)(),
|
|
4057
|
-
(0, import_richtext_lexical.StrikethroughFeature)(),
|
|
4058
|
-
// Links with enhanced configuration
|
|
4059
|
-
(0, import_richtext_lexical.LinkFeature)({
|
|
4060
|
-
fields: [
|
|
4061
|
-
{
|
|
4062
|
-
name: "url",
|
|
4063
|
-
type: "text",
|
|
4064
|
-
required: true,
|
|
4065
|
-
admin: {
|
|
4066
|
-
description: "Enter the full URL (including https://)"
|
|
4067
|
-
}
|
|
4068
|
-
},
|
|
4069
|
-
{
|
|
4070
|
-
name: "newTab",
|
|
4071
|
-
type: "checkbox",
|
|
4072
|
-
label: "Open in new tab",
|
|
4073
|
-
defaultValue: false
|
|
4008
|
+
const url = new URL(req.url || "", `http://localhost`);
|
|
4009
|
+
const pathParts = url.pathname.split("/");
|
|
4010
|
+
const id = pathParts[pathParts.length - 2];
|
|
4011
|
+
if (!id) {
|
|
4012
|
+
return Response.json({
|
|
4013
|
+
success: false,
|
|
4014
|
+
error: "Broadcast ID is required"
|
|
4015
|
+
}, { status: 400 });
|
|
4074
4016
|
}
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
admin: {
|
|
4107
|
-
description: "Alternative text for accessibility and when image cannot be displayed"
|
|
4108
|
-
}
|
|
4109
|
-
}
|
|
4110
|
-
]
|
|
4017
|
+
const data = await (req.json?.() || Promise.resolve({}));
|
|
4018
|
+
const { scheduledAt } = data;
|
|
4019
|
+
if (!scheduledAt) {
|
|
4020
|
+
return Response.json({
|
|
4021
|
+
success: false,
|
|
4022
|
+
error: "scheduledAt is required"
|
|
4023
|
+
}, { status: 400 });
|
|
4024
|
+
}
|
|
4025
|
+
const scheduledDate = new Date(scheduledAt);
|
|
4026
|
+
if (isNaN(scheduledDate.getTime())) {
|
|
4027
|
+
return Response.json({
|
|
4028
|
+
success: false,
|
|
4029
|
+
error: "Invalid scheduledAt date"
|
|
4030
|
+
}, { status: 400 });
|
|
4031
|
+
}
|
|
4032
|
+
if (scheduledDate <= /* @__PURE__ */ new Date()) {
|
|
4033
|
+
return Response.json({
|
|
4034
|
+
success: false,
|
|
4035
|
+
error: "scheduledAt must be in the future"
|
|
4036
|
+
}, { status: 400 });
|
|
4037
|
+
}
|
|
4038
|
+
const broadcastDoc = await req.payload.findByID({
|
|
4039
|
+
collection: collectionSlug,
|
|
4040
|
+
id,
|
|
4041
|
+
user: auth.user
|
|
4042
|
+
});
|
|
4043
|
+
if (!broadcastDoc || !broadcastDoc.providerId) {
|
|
4044
|
+
return Response.json({
|
|
4045
|
+
success: false,
|
|
4046
|
+
error: "Broadcast not found or not synced with provider"
|
|
4047
|
+
}, { status: 404 });
|
|
4111
4048
|
}
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
};
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
(0, import_richtext_lexical.BoldFeature)(),
|
|
4129
|
-
(0, import_richtext_lexical.ItalicFeature)(),
|
|
4130
|
-
(0, import_richtext_lexical.UnderlineFeature)(),
|
|
4131
|
-
(0, import_richtext_lexical.StrikethroughFeature)(),
|
|
4132
|
-
// Links with enhanced configuration
|
|
4133
|
-
(0, import_richtext_lexical.LinkFeature)({
|
|
4134
|
-
fields: [
|
|
4135
|
-
{
|
|
4136
|
-
name: "url",
|
|
4137
|
-
type: "text",
|
|
4138
|
-
required: true,
|
|
4139
|
-
admin: {
|
|
4140
|
-
description: "Enter the full URL (including https://)"
|
|
4141
|
-
}
|
|
4049
|
+
const providerConfig = config.providers?.broadcast;
|
|
4050
|
+
if (!providerConfig) {
|
|
4051
|
+
return Response.json({
|
|
4052
|
+
success: false,
|
|
4053
|
+
error: "Broadcast provider not configured"
|
|
4054
|
+
}, { status: 500 });
|
|
4055
|
+
}
|
|
4056
|
+
const { BroadcastApiProvider: BroadcastApiProvider2 } = await Promise.resolve().then(() => (init_broadcast2(), broadcast_exports));
|
|
4057
|
+
const provider = new BroadcastApiProvider2(providerConfig);
|
|
4058
|
+
const broadcast = await provider.schedule(broadcastDoc.providerId, scheduledDate);
|
|
4059
|
+
await req.payload.update({
|
|
4060
|
+
collection: collectionSlug,
|
|
4061
|
+
id,
|
|
4062
|
+
data: {
|
|
4063
|
+
sendStatus: "scheduled" /* SCHEDULED */,
|
|
4064
|
+
scheduledAt: scheduledDate.toISOString()
|
|
4142
4065
|
},
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
})
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
// Basic paragraph and alignment
|
|
4159
|
-
(0, import_richtext_lexical.ParagraphFeature)(),
|
|
4160
|
-
(0, import_richtext_lexical.AlignFeature)(),
|
|
4161
|
-
// Blockquotes
|
|
4162
|
-
(0, import_richtext_lexical.BlockquoteFeature)(),
|
|
4163
|
-
// Upload feature for images
|
|
4164
|
-
(0, import_richtext_lexical.UploadFeature)({
|
|
4165
|
-
collections: {
|
|
4166
|
-
media: {
|
|
4167
|
-
fields: [
|
|
4168
|
-
{
|
|
4169
|
-
name: "caption",
|
|
4170
|
-
type: "text",
|
|
4171
|
-
admin: {
|
|
4172
|
-
description: "Optional caption for the image"
|
|
4173
|
-
}
|
|
4174
|
-
},
|
|
4175
|
-
{
|
|
4176
|
-
name: "altText",
|
|
4177
|
-
type: "text",
|
|
4178
|
-
label: "Alt Text",
|
|
4179
|
-
required: true,
|
|
4180
|
-
admin: {
|
|
4181
|
-
description: "Alternative text for accessibility and when image cannot be displayed"
|
|
4182
|
-
}
|
|
4183
|
-
}
|
|
4184
|
-
]
|
|
4185
|
-
}
|
|
4066
|
+
user: auth.user
|
|
4067
|
+
});
|
|
4068
|
+
return Response.json({
|
|
4069
|
+
success: true,
|
|
4070
|
+
message: `Broadcast scheduled for ${scheduledDate.toISOString()}`,
|
|
4071
|
+
broadcast
|
|
4072
|
+
});
|
|
4073
|
+
} catch (error) {
|
|
4074
|
+
console.error("Failed to schedule broadcast:", error);
|
|
4075
|
+
if (error instanceof NewsletterProviderError) {
|
|
4076
|
+
return Response.json({
|
|
4077
|
+
success: false,
|
|
4078
|
+
error: error.message,
|
|
4079
|
+
code: error.code
|
|
4080
|
+
}, { status: error.code === "NOT_SUPPORTED" ? 501 : 500 });
|
|
4186
4081
|
}
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
}
|
|
4192
|
-
|
|
4193
|
-
}
|
|
4082
|
+
return Response.json({
|
|
4083
|
+
success: false,
|
|
4084
|
+
error: "Failed to schedule broadcast"
|
|
4085
|
+
}, { status: 500 });
|
|
4086
|
+
}
|
|
4087
|
+
}
|
|
4088
|
+
};
|
|
4194
4089
|
};
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4090
|
+
|
|
4091
|
+
// src/endpoints/broadcasts/test.ts
|
|
4092
|
+
var createTestBroadcastEndpoint = (config, collectionSlug) => {
|
|
4198
4093
|
return {
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4094
|
+
path: "/:id/test",
|
|
4095
|
+
method: "post",
|
|
4096
|
+
handler: async (req) => {
|
|
4097
|
+
try {
|
|
4098
|
+
const auth = await requireAdmin(req, config);
|
|
4099
|
+
if (!auth.authorized) {
|
|
4100
|
+
return Response.json({
|
|
4101
|
+
success: false,
|
|
4102
|
+
error: auth.error
|
|
4103
|
+
}, { status: 401 });
|
|
4104
|
+
}
|
|
4105
|
+
const url = new URL(req.url || "", `http://localhost`);
|
|
4106
|
+
const pathParts = url.pathname.split("/");
|
|
4107
|
+
const id = pathParts[pathParts.length - 2];
|
|
4108
|
+
if (!id) {
|
|
4109
|
+
return Response.json({
|
|
4110
|
+
success: false,
|
|
4111
|
+
error: "Broadcast ID is required"
|
|
4112
|
+
}, { status: 400 });
|
|
4113
|
+
}
|
|
4114
|
+
const data = await (req.json?.() || Promise.resolve({}));
|
|
4115
|
+
const testEmail = data.email || auth.user.email;
|
|
4116
|
+
if (!testEmail) {
|
|
4117
|
+
return Response.json({
|
|
4118
|
+
success: false,
|
|
4119
|
+
error: "No email address available for test send"
|
|
4120
|
+
}, { status: 400 });
|
|
4121
|
+
}
|
|
4122
|
+
const broadcast = await req.payload.findByID({
|
|
4123
|
+
collection: collectionSlug,
|
|
4124
|
+
id,
|
|
4125
|
+
user: auth.user
|
|
4126
|
+
});
|
|
4127
|
+
if (!broadcast) {
|
|
4128
|
+
return Response.json({
|
|
4129
|
+
success: false,
|
|
4130
|
+
error: "Broadcast not found"
|
|
4131
|
+
}, { status: 404 });
|
|
4132
|
+
}
|
|
4133
|
+
const htmlContent = await convertToEmailSafeHtml(broadcast.content, {
|
|
4134
|
+
wrapInTemplate: true,
|
|
4135
|
+
preheader: broadcast.preheader,
|
|
4136
|
+
customBlockConverter: config.customizations?.broadcasts?.customBlockConverter
|
|
4137
|
+
});
|
|
4138
|
+
const emailService = req.payload.newsletterEmailService;
|
|
4139
|
+
if (!emailService) {
|
|
4140
|
+
return Response.json({
|
|
4141
|
+
success: false,
|
|
4142
|
+
error: "Email service is not configured"
|
|
4143
|
+
}, { status: 500 });
|
|
4144
|
+
}
|
|
4145
|
+
const providerConfig = config.providers.default === "resend" ? config.providers.resend : config.providers.broadcast;
|
|
4146
|
+
const fromEmail = providerConfig?.fromAddress || providerConfig?.fromEmail || "noreply@example.com";
|
|
4147
|
+
const fromName = providerConfig?.fromName || "Newsletter";
|
|
4148
|
+
const replyTo = broadcast.settings?.replyTo || providerConfig?.replyTo;
|
|
4149
|
+
await emailService.send({
|
|
4150
|
+
to: testEmail,
|
|
4151
|
+
from: fromEmail,
|
|
4152
|
+
fromName,
|
|
4153
|
+
replyTo,
|
|
4154
|
+
subject: `[TEST] ${broadcast.subject}`,
|
|
4155
|
+
html: htmlContent,
|
|
4156
|
+
trackOpens: false,
|
|
4157
|
+
trackClicks: false
|
|
4158
|
+
});
|
|
4159
|
+
return Response.json({
|
|
4160
|
+
success: true,
|
|
4161
|
+
message: `Test email sent to ${testEmail}`
|
|
4162
|
+
});
|
|
4163
|
+
} catch (error) {
|
|
4164
|
+
console.error("Failed to send test broadcast:", error);
|
|
4165
|
+
return Response.json({
|
|
4166
|
+
success: false,
|
|
4167
|
+
error: "Failed to send test email"
|
|
4168
|
+
}, { status: 500 });
|
|
4169
|
+
}
|
|
4170
|
+
}
|
|
4208
4171
|
};
|
|
4209
4172
|
};
|
|
4210
4173
|
|
|
4211
|
-
// src/
|
|
4212
|
-
var
|
|
4174
|
+
// src/endpoints/broadcasts/preview.ts
|
|
4175
|
+
var createBroadcastPreviewEndpoint = (config, collectionSlug) => {
|
|
4213
4176
|
return {
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4177
|
+
path: "/preview",
|
|
4178
|
+
method: "post",
|
|
4179
|
+
handler: async (req) => {
|
|
4180
|
+
try {
|
|
4181
|
+
const data = await (req.json?.() || Promise.resolve({}));
|
|
4182
|
+
const { content, preheader, subject } = data;
|
|
4183
|
+
if (!content) {
|
|
4184
|
+
return Response.json({
|
|
4185
|
+
success: false,
|
|
4186
|
+
error: "Content is required for preview"
|
|
4187
|
+
}, { status: 400 });
|
|
4188
|
+
}
|
|
4189
|
+
const mediaUrl = req.payload.config.serverURL ? `${req.payload.config.serverURL}/api/media` : "/api/media";
|
|
4190
|
+
const htmlContent = await convertToEmailSafeHtml(content, {
|
|
4191
|
+
wrapInTemplate: true,
|
|
4192
|
+
preheader,
|
|
4193
|
+
mediaUrl,
|
|
4194
|
+
customBlockConverter: config.customizations?.broadcasts?.customBlockConverter
|
|
4195
|
+
});
|
|
4196
|
+
return Response.json({
|
|
4197
|
+
success: true,
|
|
4198
|
+
preview: {
|
|
4199
|
+
subject: subject || "Preview",
|
|
4200
|
+
preheader: preheader || "",
|
|
4201
|
+
html: htmlContent
|
|
4202
|
+
}
|
|
4203
|
+
});
|
|
4204
|
+
} catch (error) {
|
|
4205
|
+
console.error("Failed to generate email preview:", error);
|
|
4206
|
+
return Response.json({
|
|
4207
|
+
success: false,
|
|
4208
|
+
error: "Failed to generate email preview"
|
|
4209
|
+
}, { status: 500 });
|
|
4219
4210
|
}
|
|
4220
4211
|
}
|
|
4221
4212
|
};
|
|
@@ -4225,8 +4216,9 @@ var createBroadcastInlinePreviewField = () => {
|
|
|
4225
4216
|
var createBroadcastsCollection = (pluginConfig) => {
|
|
4226
4217
|
const hasProviders = !!(pluginConfig.providers?.broadcast || pluginConfig.providers?.resend);
|
|
4227
4218
|
const customizations = pluginConfig.customizations?.broadcasts;
|
|
4219
|
+
const collectionSlug = "broadcasts";
|
|
4228
4220
|
return {
|
|
4229
|
-
slug:
|
|
4221
|
+
slug: collectionSlug,
|
|
4230
4222
|
access: {
|
|
4231
4223
|
read: () => true,
|
|
4232
4224
|
// Public read access
|
|
@@ -4255,6 +4247,12 @@ var createBroadcastsCollection = (pluginConfig) => {
|
|
|
4255
4247
|
description: "Individual email campaigns sent to subscribers",
|
|
4256
4248
|
defaultColumns: ["subject", "_status", "sendStatus", "sentAt", "recipientCount"]
|
|
4257
4249
|
},
|
|
4250
|
+
endpoints: [
|
|
4251
|
+
createSendBroadcastEndpoint(pluginConfig, collectionSlug),
|
|
4252
|
+
createScheduleBroadcastEndpoint(pluginConfig, collectionSlug),
|
|
4253
|
+
createTestBroadcastEndpoint(pluginConfig, collectionSlug),
|
|
4254
|
+
createBroadcastPreviewEndpoint(pluginConfig, collectionSlug)
|
|
4255
|
+
],
|
|
4258
4256
|
fields: [
|
|
4259
4257
|
{
|
|
4260
4258
|
name: "subject",
|