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.js
CHANGED
|
@@ -2856,270 +2856,677 @@ var createSignoutEndpoint = (_config) => {
|
|
|
2856
2856
|
};
|
|
2857
2857
|
};
|
|
2858
2858
|
|
|
2859
|
-
// src/endpoints/broadcasts/
|
|
2860
|
-
|
|
2859
|
+
// src/endpoints/broadcasts/index.ts
|
|
2860
|
+
var createBroadcastManagementEndpoints = (config) => {
|
|
2861
|
+
return [];
|
|
2862
|
+
};
|
|
2861
2863
|
|
|
2862
|
-
// src/
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
} catch {
|
|
2878
|
-
return null;
|
|
2879
|
-
}
|
|
2880
|
-
}
|
|
2881
|
-
async function requireAdmin(req, config) {
|
|
2882
|
-
const user = await getAuthenticatedUser(req);
|
|
2883
|
-
if (!user) {
|
|
2884
|
-
return {
|
|
2885
|
-
authorized: false,
|
|
2886
|
-
error: "Authentication required"
|
|
2887
|
-
};
|
|
2888
|
-
}
|
|
2889
|
-
if (!isAdmin(user, config)) {
|
|
2890
|
-
return {
|
|
2891
|
-
authorized: false,
|
|
2892
|
-
error: "Admin access required"
|
|
2893
|
-
};
|
|
2864
|
+
// src/endpoints/index.ts
|
|
2865
|
+
function createNewsletterEndpoints(config) {
|
|
2866
|
+
const endpoints = [
|
|
2867
|
+
createSubscribeEndpoint(config),
|
|
2868
|
+
createUnsubscribeEndpoint(config)
|
|
2869
|
+
];
|
|
2870
|
+
if (config.auth?.enabled !== false) {
|
|
2871
|
+
endpoints.push(
|
|
2872
|
+
createVerifyMagicLinkEndpoint(config),
|
|
2873
|
+
createPreferencesEndpoint(config),
|
|
2874
|
+
createUpdatePreferencesEndpoint(config),
|
|
2875
|
+
createSigninEndpoint(config),
|
|
2876
|
+
createMeEndpoint(config),
|
|
2877
|
+
createSignoutEndpoint(config)
|
|
2878
|
+
);
|
|
2894
2879
|
}
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
user
|
|
2898
|
-
};
|
|
2880
|
+
endpoints.push(...createBroadcastManagementEndpoints(config));
|
|
2881
|
+
return endpoints;
|
|
2899
2882
|
}
|
|
2900
2883
|
|
|
2901
|
-
// src/
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2884
|
+
// src/fields/newsletterScheduling.ts
|
|
2885
|
+
function createNewsletterSchedulingFields(config) {
|
|
2886
|
+
const groupName = config.features?.newsletterScheduling?.fields?.groupName || "newsletterScheduling";
|
|
2887
|
+
const contentField = config.features?.newsletterScheduling?.fields?.contentField || "content";
|
|
2888
|
+
const createMarkdownField = config.features?.newsletterScheduling?.fields?.createMarkdownField !== false;
|
|
2889
|
+
const fields = [
|
|
2890
|
+
{
|
|
2891
|
+
name: groupName,
|
|
2892
|
+
type: "group",
|
|
2893
|
+
label: "Newsletter Scheduling",
|
|
2894
|
+
admin: {
|
|
2895
|
+
condition: (data, { user }) => user?.collection === "users"
|
|
2896
|
+
// Only show for admin users
|
|
2897
|
+
},
|
|
2898
|
+
fields: [
|
|
2899
|
+
{
|
|
2900
|
+
name: "scheduled",
|
|
2901
|
+
type: "checkbox",
|
|
2902
|
+
label: "Schedule for Newsletter",
|
|
2903
|
+
defaultValue: false,
|
|
2904
|
+
admin: {
|
|
2905
|
+
description: "Schedule this content to be sent as a newsletter"
|
|
2906
|
+
}
|
|
2907
|
+
},
|
|
2908
|
+
{
|
|
2909
|
+
name: "scheduledDate",
|
|
2910
|
+
type: "date",
|
|
2911
|
+
label: "Send Date",
|
|
2912
|
+
required: true,
|
|
2913
|
+
admin: {
|
|
2914
|
+
date: {
|
|
2915
|
+
pickerAppearance: "dayAndTime"
|
|
2916
|
+
},
|
|
2917
|
+
condition: (data) => data?.[groupName]?.scheduled,
|
|
2918
|
+
description: "When to send this newsletter"
|
|
2919
|
+
}
|
|
2920
|
+
},
|
|
2921
|
+
{
|
|
2922
|
+
name: "sentDate",
|
|
2923
|
+
type: "date",
|
|
2924
|
+
label: "Sent Date",
|
|
2925
|
+
admin: {
|
|
2926
|
+
readOnly: true,
|
|
2927
|
+
condition: (data) => data?.[groupName]?.sendStatus === "sent",
|
|
2928
|
+
description: "When this newsletter was sent"
|
|
2929
|
+
}
|
|
2930
|
+
},
|
|
2931
|
+
{
|
|
2932
|
+
name: "sendStatus",
|
|
2933
|
+
type: "select",
|
|
2934
|
+
label: "Status",
|
|
2935
|
+
options: [
|
|
2936
|
+
{ label: "Draft", value: "draft" },
|
|
2937
|
+
{ label: "Scheduled", value: "scheduled" },
|
|
2938
|
+
{ label: "Sending", value: "sending" },
|
|
2939
|
+
{ label: "Sent", value: "sent" },
|
|
2940
|
+
{ label: "Failed", value: "failed" }
|
|
2941
|
+
],
|
|
2942
|
+
defaultValue: "draft",
|
|
2943
|
+
admin: {
|
|
2944
|
+
readOnly: true,
|
|
2945
|
+
description: "Current send status"
|
|
2946
|
+
}
|
|
2947
|
+
},
|
|
2948
|
+
{
|
|
2949
|
+
name: "emailSubject",
|
|
2950
|
+
type: "text",
|
|
2951
|
+
label: "Email Subject",
|
|
2952
|
+
required: true,
|
|
2953
|
+
admin: {
|
|
2954
|
+
condition: (data) => data?.[groupName]?.scheduled,
|
|
2955
|
+
description: "Subject line for the newsletter email"
|
|
2956
|
+
}
|
|
2957
|
+
},
|
|
2958
|
+
{
|
|
2959
|
+
name: "preheader",
|
|
2960
|
+
type: "text",
|
|
2961
|
+
label: "Email Preheader",
|
|
2962
|
+
admin: {
|
|
2963
|
+
condition: (data) => data?.[groupName]?.scheduled,
|
|
2964
|
+
description: "Preview text that appears after the subject line"
|
|
2965
|
+
}
|
|
2966
|
+
},
|
|
2967
|
+
{
|
|
2968
|
+
name: "segments",
|
|
2969
|
+
type: "select",
|
|
2970
|
+
label: "Target Segments",
|
|
2971
|
+
hasMany: true,
|
|
2972
|
+
options: [
|
|
2973
|
+
{ label: "All Subscribers", value: "all" },
|
|
2974
|
+
...config.i18n?.locales?.map((locale) => ({
|
|
2975
|
+
label: `${locale.toUpperCase()} Subscribers`,
|
|
2976
|
+
value: locale
|
|
2977
|
+
})) || []
|
|
2978
|
+
],
|
|
2979
|
+
defaultValue: ["all"],
|
|
2980
|
+
admin: {
|
|
2981
|
+
condition: (data) => data?.[groupName]?.scheduled,
|
|
2982
|
+
description: "Which subscriber segments to send to"
|
|
2983
|
+
}
|
|
2984
|
+
},
|
|
2985
|
+
{
|
|
2986
|
+
name: "testEmails",
|
|
2987
|
+
type: "array",
|
|
2988
|
+
label: "Test Email Recipients",
|
|
2989
|
+
admin: {
|
|
2990
|
+
condition: (data) => data?.[groupName]?.scheduled && data?.[groupName]?.sendStatus === "draft",
|
|
2991
|
+
description: "Send test emails before scheduling"
|
|
2992
|
+
},
|
|
2993
|
+
fields: [
|
|
2994
|
+
{
|
|
2995
|
+
name: "email",
|
|
2996
|
+
type: "email",
|
|
2997
|
+
required: true
|
|
2998
|
+
}
|
|
2999
|
+
]
|
|
3000
|
+
}
|
|
3001
|
+
]
|
|
2916
3002
|
}
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
3003
|
+
];
|
|
3004
|
+
if (createMarkdownField) {
|
|
3005
|
+
fields.push(createMarkdownFieldInternal({
|
|
3006
|
+
name: `${contentField}Markdown`,
|
|
3007
|
+
richTextField: contentField,
|
|
3008
|
+
label: "Email Content (Markdown)",
|
|
3009
|
+
admin: {
|
|
3010
|
+
position: "sidebar",
|
|
3011
|
+
condition: (data) => Boolean(data?.[contentField] && data?.[groupName]?.scheduled),
|
|
3012
|
+
description: "Markdown version for email rendering",
|
|
3013
|
+
readOnly: true
|
|
3014
|
+
}
|
|
3015
|
+
}));
|
|
2921
3016
|
}
|
|
3017
|
+
return fields;
|
|
2922
3018
|
}
|
|
2923
|
-
|
|
2924
|
-
// src/endpoints/broadcasts/send.ts
|
|
2925
|
-
var createSendBroadcastEndpoint = (config, collectionSlug) => {
|
|
3019
|
+
function createMarkdownFieldInternal(config) {
|
|
2926
3020
|
return {
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
3021
|
+
name: config.name,
|
|
3022
|
+
type: "textarea",
|
|
3023
|
+
label: config.label || "Markdown",
|
|
3024
|
+
admin: {
|
|
3025
|
+
...config.admin,
|
|
3026
|
+
description: config.admin?.description || "Auto-generated from rich text content"
|
|
3027
|
+
},
|
|
3028
|
+
hooks: {
|
|
3029
|
+
afterRead: [
|
|
3030
|
+
async ({ data }) => {
|
|
3031
|
+
if (data?.[config.richTextField]) {
|
|
3032
|
+
try {
|
|
3033
|
+
const { convertLexicalToMarkdown } = await import("@payloadcms/richtext-lexical");
|
|
3034
|
+
return convertLexicalToMarkdown({
|
|
3035
|
+
data: data[config.richTextField]
|
|
3036
|
+
});
|
|
3037
|
+
} catch {
|
|
3038
|
+
return "";
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
return "";
|
|
2937
3042
|
}
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
}, { status: 400 });
|
|
3043
|
+
],
|
|
3044
|
+
beforeChange: [
|
|
3045
|
+
() => {
|
|
3046
|
+
return null;
|
|
2943
3047
|
}
|
|
2944
|
-
|
|
2945
|
-
const pathParts = url.pathname.split("/");
|
|
2946
|
-
const id = pathParts[pathParts.length - 2];
|
|
2947
|
-
if (!id) {
|
|
2948
|
-
return Response.json({
|
|
2949
|
-
success: false,
|
|
2950
|
-
error: "Broadcast ID is required"
|
|
2951
|
-
}, { status: 400 });
|
|
2952
|
-
}
|
|
2953
|
-
const data = await (req.json?.() || Promise.resolve({}));
|
|
2954
|
-
const broadcastDoc = await req.payload.findByID({
|
|
2955
|
-
collection: collectionSlug,
|
|
2956
|
-
id,
|
|
2957
|
-
user: auth.user
|
|
2958
|
-
});
|
|
2959
|
-
if (!broadcastDoc || !broadcastDoc.providerId) {
|
|
2960
|
-
return Response.json({
|
|
2961
|
-
success: false,
|
|
2962
|
-
error: "Broadcast not found or not synced with provider"
|
|
2963
|
-
}, { status: 404 });
|
|
2964
|
-
}
|
|
2965
|
-
const providerConfig = await getBroadcastConfig(req, config);
|
|
2966
|
-
if (!providerConfig || !providerConfig.token) {
|
|
2967
|
-
return Response.json({
|
|
2968
|
-
success: false,
|
|
2969
|
-
error: "Broadcast provider not configured in Newsletter Settings or environment variables"
|
|
2970
|
-
}, { status: 500 });
|
|
2971
|
-
}
|
|
2972
|
-
const { BroadcastApiProvider: BroadcastApiProvider2 } = await Promise.resolve().then(() => (init_broadcast2(), broadcast_exports));
|
|
2973
|
-
const provider = new BroadcastApiProvider2(providerConfig);
|
|
2974
|
-
const broadcast = await provider.send(broadcastDoc.providerId, data);
|
|
2975
|
-
await req.payload.update({
|
|
2976
|
-
collection: collectionSlug,
|
|
2977
|
-
id,
|
|
2978
|
-
data: {
|
|
2979
|
-
sendStatus: "sending" /* SENDING */,
|
|
2980
|
-
sentAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2981
|
-
},
|
|
2982
|
-
user: auth.user
|
|
2983
|
-
});
|
|
2984
|
-
return Response.json({
|
|
2985
|
-
success: true,
|
|
2986
|
-
message: "Broadcast sent successfully",
|
|
2987
|
-
broadcast
|
|
2988
|
-
});
|
|
2989
|
-
} catch (error) {
|
|
2990
|
-
console.error("Failed to send broadcast:", error);
|
|
2991
|
-
if (error instanceof NewsletterProviderError) {
|
|
2992
|
-
return Response.json({
|
|
2993
|
-
success: false,
|
|
2994
|
-
error: error.message,
|
|
2995
|
-
code: error.code
|
|
2996
|
-
}, { status: error.code === "NOT_SUPPORTED" ? 501 : 500 });
|
|
2997
|
-
}
|
|
2998
|
-
return Response.json({
|
|
2999
|
-
success: false,
|
|
3000
|
-
error: "Failed to send broadcast"
|
|
3001
|
-
}, { status: 500 });
|
|
3002
|
-
}
|
|
3048
|
+
]
|
|
3003
3049
|
}
|
|
3004
3050
|
};
|
|
3005
|
-
}
|
|
3051
|
+
}
|
|
3006
3052
|
|
|
3007
|
-
// src/
|
|
3008
|
-
|
|
3009
|
-
var createScheduleBroadcastEndpoint = (config, collectionSlug) => {
|
|
3053
|
+
// src/jobs/sync-unsubscribes.ts
|
|
3054
|
+
var createUnsubscribeSyncJob = (pluginConfig) => {
|
|
3010
3055
|
return {
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
handler: async (req) => {
|
|
3056
|
+
slug: "sync-unsubscribes",
|
|
3057
|
+
label: "Sync Unsubscribes from Email Service",
|
|
3058
|
+
handler: async ({ req }) => {
|
|
3059
|
+
const subscribersSlug = pluginConfig.subscribersSlug || "subscribers";
|
|
3060
|
+
const emailService = req.payload.newsletterEmailService;
|
|
3061
|
+
if (!emailService) {
|
|
3062
|
+
console.error("Email service not configured");
|
|
3063
|
+
return {
|
|
3064
|
+
output: {
|
|
3065
|
+
syncedCount: 0
|
|
3066
|
+
}
|
|
3067
|
+
};
|
|
3068
|
+
}
|
|
3069
|
+
let syncedCount = 0;
|
|
3014
3070
|
try {
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
}
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
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
|
-
|
|
3071
|
+
if (emailService.getProvider() === "broadcast") {
|
|
3072
|
+
console.warn("Starting Broadcast unsubscribe sync...");
|
|
3073
|
+
const broadcastConfig = pluginConfig.providers?.broadcast;
|
|
3074
|
+
if (!broadcastConfig) {
|
|
3075
|
+
throw new Error("Broadcast configuration not found");
|
|
3076
|
+
}
|
|
3077
|
+
const apiUrl = broadcastConfig.apiUrl.replace(/\/$/, "");
|
|
3078
|
+
const token = broadcastConfig.token;
|
|
3079
|
+
let page = 1;
|
|
3080
|
+
let hasMore = true;
|
|
3081
|
+
while (hasMore) {
|
|
3082
|
+
const response = await fetch(
|
|
3083
|
+
`${apiUrl}/api/v1/subscribers.json?page=${page}`,
|
|
3084
|
+
{
|
|
3085
|
+
headers: {
|
|
3086
|
+
"Authorization": `Bearer ${token}`
|
|
3087
|
+
}
|
|
3088
|
+
}
|
|
3089
|
+
);
|
|
3090
|
+
if (!response.ok) {
|
|
3091
|
+
throw new Error(`Broadcast API error: ${response.status}`);
|
|
3092
|
+
}
|
|
3093
|
+
const data = await response.json();
|
|
3094
|
+
const broadcastSubscribers = data.subscribers || [];
|
|
3095
|
+
for (const broadcastSub of broadcastSubscribers) {
|
|
3096
|
+
const payloadSubscribers = await req.payload.find({
|
|
3097
|
+
collection: subscribersSlug,
|
|
3098
|
+
where: {
|
|
3099
|
+
email: {
|
|
3100
|
+
equals: broadcastSub.email
|
|
3101
|
+
}
|
|
3102
|
+
},
|
|
3103
|
+
limit: 1
|
|
3104
|
+
});
|
|
3105
|
+
if (payloadSubscribers.docs.length > 0) {
|
|
3106
|
+
const payloadSub = payloadSubscribers.docs[0];
|
|
3107
|
+
const broadcastUnsubscribed = !broadcastSub.is_active || broadcastSub.unsubscribed_at;
|
|
3108
|
+
const payloadUnsubscribed = payloadSub.subscriptionStatus === "unsubscribed";
|
|
3109
|
+
if (broadcastUnsubscribed && !payloadUnsubscribed) {
|
|
3110
|
+
await req.payload.update({
|
|
3111
|
+
collection: subscribersSlug,
|
|
3112
|
+
id: payloadSub.id,
|
|
3113
|
+
data: {
|
|
3114
|
+
subscriptionStatus: "unsubscribed",
|
|
3115
|
+
unsubscribedAt: broadcastSub.unsubscribed_at || (/* @__PURE__ */ new Date()).toISOString()
|
|
3116
|
+
}
|
|
3117
|
+
});
|
|
3118
|
+
syncedCount++;
|
|
3119
|
+
console.warn(`Unsubscribed: ${broadcastSub.email}`);
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
if (data.pagination && data.pagination.current < data.pagination.total_pages) {
|
|
3124
|
+
page++;
|
|
3125
|
+
} else {
|
|
3126
|
+
hasMore = false;
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
console.warn(`Broadcast sync complete. Unsubscribed ${syncedCount} contacts.`);
|
|
3057
3130
|
}
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
success: false,
|
|
3066
|
-
error: "Broadcast not found or not synced with provider"
|
|
3067
|
-
}, { status: 404 });
|
|
3131
|
+
if (emailService.getProvider() === "resend") {
|
|
3132
|
+
console.warn("Starting Resend unsubscribe sync...");
|
|
3133
|
+
const resendConfig = pluginConfig.providers?.resend;
|
|
3134
|
+
if (!resendConfig) {
|
|
3135
|
+
throw new Error("Resend configuration not found");
|
|
3136
|
+
}
|
|
3137
|
+
console.warn("Resend polling implementation needed - webhooks recommended");
|
|
3068
3138
|
}
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
}, { status: 500 });
|
|
3139
|
+
if (pluginConfig.hooks?.afterUnsubscribeSync) {
|
|
3140
|
+
await pluginConfig.hooks.afterUnsubscribeSync({
|
|
3141
|
+
req,
|
|
3142
|
+
syncedCount
|
|
3143
|
+
});
|
|
3075
3144
|
}
|
|
3076
|
-
const { BroadcastApiProvider: BroadcastApiProvider2 } = await Promise.resolve().then(() => (init_broadcast2(), broadcast_exports));
|
|
3077
|
-
const provider = new BroadcastApiProvider2(providerConfig);
|
|
3078
|
-
const broadcast = await provider.schedule(broadcastDoc.providerId, scheduledDate);
|
|
3079
|
-
await req.payload.update({
|
|
3080
|
-
collection: collectionSlug,
|
|
3081
|
-
id,
|
|
3082
|
-
data: {
|
|
3083
|
-
sendStatus: "scheduled" /* SCHEDULED */,
|
|
3084
|
-
scheduledAt: scheduledDate.toISOString()
|
|
3085
|
-
},
|
|
3086
|
-
user: auth.user
|
|
3087
|
-
});
|
|
3088
|
-
return Response.json({
|
|
3089
|
-
success: true,
|
|
3090
|
-
message: `Broadcast scheduled for ${scheduledDate.toISOString()}`,
|
|
3091
|
-
broadcast
|
|
3092
|
-
});
|
|
3093
3145
|
} catch (error) {
|
|
3094
|
-
console.error("
|
|
3095
|
-
|
|
3096
|
-
return Response.json({
|
|
3097
|
-
success: false,
|
|
3098
|
-
error: error.message,
|
|
3099
|
-
code: error.code
|
|
3100
|
-
}, { status: error.code === "NOT_SUPPORTED" ? 501 : 500 });
|
|
3101
|
-
}
|
|
3102
|
-
return Response.json({
|
|
3103
|
-
success: false,
|
|
3104
|
-
error: "Failed to schedule broadcast"
|
|
3105
|
-
}, { status: 500 });
|
|
3146
|
+
console.error("Unsubscribe sync error:", error);
|
|
3147
|
+
throw error;
|
|
3106
3148
|
}
|
|
3149
|
+
return {
|
|
3150
|
+
output: {
|
|
3151
|
+
syncedCount
|
|
3152
|
+
}
|
|
3153
|
+
};
|
|
3107
3154
|
}
|
|
3108
3155
|
};
|
|
3109
3156
|
};
|
|
3110
3157
|
|
|
3111
|
-
// src/
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3158
|
+
// src/collections/Broadcasts.ts
|
|
3159
|
+
init_types();
|
|
3160
|
+
|
|
3161
|
+
// src/fields/emailContent.ts
|
|
3162
|
+
import {
|
|
3163
|
+
BoldFeature,
|
|
3164
|
+
ItalicFeature,
|
|
3165
|
+
UnderlineFeature,
|
|
3166
|
+
StrikethroughFeature,
|
|
3167
|
+
LinkFeature,
|
|
3168
|
+
OrderedListFeature,
|
|
3169
|
+
UnorderedListFeature,
|
|
3170
|
+
HeadingFeature,
|
|
3171
|
+
ParagraphFeature,
|
|
3172
|
+
AlignFeature,
|
|
3173
|
+
BlockquoteFeature,
|
|
3174
|
+
BlocksFeature,
|
|
3175
|
+
UploadFeature,
|
|
3176
|
+
FixedToolbarFeature,
|
|
3177
|
+
InlineToolbarFeature,
|
|
3178
|
+
lexicalEditor
|
|
3179
|
+
} from "@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
|
+
FixedToolbarFeature(),
|
|
3344
|
+
// Fixed toolbar at the top
|
|
3345
|
+
InlineToolbarFeature(),
|
|
3346
|
+
// Floating toolbar when text is selected
|
|
3347
|
+
// Basic text formatting
|
|
3348
|
+
BoldFeature(),
|
|
3349
|
+
ItalicFeature(),
|
|
3350
|
+
UnderlineFeature(),
|
|
3351
|
+
StrikethroughFeature(),
|
|
3352
|
+
// Links with enhanced configuration
|
|
3353
|
+
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
|
+
OrderedListFeature(),
|
|
3373
|
+
UnorderedListFeature(),
|
|
3374
|
+
// Headings - limited to h1, h2, h3 for email compatibility
|
|
3375
|
+
HeadingFeature({
|
|
3376
|
+
enabledHeadingSizes: ["h1", "h2", "h3"]
|
|
3377
|
+
}),
|
|
3378
|
+
// Basic paragraph and alignment
|
|
3379
|
+
ParagraphFeature(),
|
|
3380
|
+
AlignFeature(),
|
|
3381
|
+
// Blockquotes
|
|
3382
|
+
BlockquoteFeature(),
|
|
3383
|
+
// Upload feature for images
|
|
3384
|
+
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
|
+
BlocksFeature({
|
|
3410
|
+
blocks: allBlocks
|
|
3411
|
+
})
|
|
3412
|
+
];
|
|
3413
|
+
};
|
|
3414
|
+
var createEmailLexicalEditor = (customBlocks = []) => {
|
|
3415
|
+
const emailSafeBlocks = createEmailSafeBlocks(customBlocks);
|
|
3416
|
+
return lexicalEditor({
|
|
3417
|
+
features: [
|
|
3418
|
+
// Toolbars
|
|
3419
|
+
FixedToolbarFeature(),
|
|
3420
|
+
InlineToolbarFeature(),
|
|
3421
|
+
// Basic text formatting
|
|
3422
|
+
BoldFeature(),
|
|
3423
|
+
ItalicFeature(),
|
|
3424
|
+
UnderlineFeature(),
|
|
3425
|
+
StrikethroughFeature(),
|
|
3426
|
+
// Links with enhanced configuration
|
|
3427
|
+
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
|
+
OrderedListFeature(),
|
|
3447
|
+
UnorderedListFeature(),
|
|
3448
|
+
// Headings - limited to h1, h2, h3 for email compatibility
|
|
3449
|
+
HeadingFeature({
|
|
3450
|
+
enabledHeadingSizes: ["h1", "h2", "h3"]
|
|
3451
|
+
}),
|
|
3452
|
+
// Basic paragraph and alignment
|
|
3453
|
+
ParagraphFeature(),
|
|
3454
|
+
AlignFeature(),
|
|
3455
|
+
// Blockquotes
|
|
3456
|
+
BlockquoteFeature(),
|
|
3457
|
+
// Upload feature for images
|
|
3458
|
+
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
|
+
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
|
+
import DOMPurify2 from "isomorphic-dompurify";
|
|
3520
|
+
var EMAIL_SAFE_CONFIG = {
|
|
3521
|
+
ALLOWED_TAGS: [
|
|
3522
|
+
"p",
|
|
3523
|
+
"br",
|
|
3524
|
+
"strong",
|
|
3525
|
+
"b",
|
|
3526
|
+
"em",
|
|
3527
|
+
"i",
|
|
3528
|
+
"u",
|
|
3529
|
+
"strike",
|
|
3123
3530
|
"s",
|
|
3124
3531
|
"span",
|
|
3125
3532
|
"a",
|
|
@@ -3363,859 +3770,443 @@ function convertButtonBlock(fields) {
|
|
|
3363
3770
|
const buttonStyle = `${styles2[style] || styles2.primary} display: inline-block; padding: 12px 24px; font-size: 16px; font-weight: 600; text-decoration: none; border-radius: 6px; text-align: center;`;
|
|
3364
3771
|
return `
|
|
3365
3772
|
<div style="margin: 0 0 16px 0; text-align: center;">
|
|
3366
|
-
<a href="${escapeHtml(url)}" target="_blank" rel="noopener noreferrer" style="${buttonStyle}">${escapeHtml(text)}</a>
|
|
3367
|
-
</div>
|
|
3368
|
-
`;
|
|
3369
|
-
}
|
|
3370
|
-
function convertDividerBlock(fields) {
|
|
3371
|
-
const style = fields?.style || "solid";
|
|
3372
|
-
const styles2 = {
|
|
3373
|
-
solid: "border-top: 1px solid #e5e7eb;",
|
|
3374
|
-
dashed: "border-top: 1px dashed #e5e7eb;",
|
|
3375
|
-
dotted: "border-top: 1px dotted #e5e7eb;"
|
|
3376
|
-
};
|
|
3377
|
-
return `<hr style="${styles2[style] || styles2.solid} margin: 24px 0; border-bottom: none; border-left: none; border-right: none;" />`;
|
|
3378
|
-
}
|
|
3379
|
-
function getAlignment(format) {
|
|
3380
|
-
if (!format) return "left";
|
|
3381
|
-
if (format & 2) return "center";
|
|
3382
|
-
if (format & 3) return "right";
|
|
3383
|
-
if (format & 4) return "justify";
|
|
3384
|
-
return "left";
|
|
3385
|
-
}
|
|
3386
|
-
function escapeHtml(text) {
|
|
3387
|
-
const map = {
|
|
3388
|
-
"&": "&",
|
|
3389
|
-
"<": "<",
|
|
3390
|
-
">": ">",
|
|
3391
|
-
'"': """,
|
|
3392
|
-
"'": "'"
|
|
3393
|
-
};
|
|
3394
|
-
return text.replace(/[&<>"']/g, (m) => map[m]);
|
|
3395
|
-
}
|
|
3396
|
-
function wrapInEmailTemplate(content, preheader) {
|
|
3397
|
-
return `<!DOCTYPE html>
|
|
3398
|
-
<html lang="en">
|
|
3399
|
-
<head>
|
|
3400
|
-
<meta charset="UTF-8">
|
|
3401
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
3402
|
-
<title>Email</title>
|
|
3403
|
-
<!--[if mso]>
|
|
3404
|
-
<noscript>
|
|
3405
|
-
<xml>
|
|
3406
|
-
<o:OfficeDocumentSettings>
|
|
3407
|
-
<o:PixelsPerInch>96</o:PixelsPerInch>
|
|
3408
|
-
</o:OfficeDocumentSettings>
|
|
3409
|
-
</xml>
|
|
3410
|
-
</noscript>
|
|
3411
|
-
<![endif]-->
|
|
3412
|
-
</head>
|
|
3413
|
-
<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;">
|
|
3414
|
-
${preheader ? `<div style="display: none; max-height: 0; overflow: hidden;">${escapeHtml(preheader)}</div>` : ""}
|
|
3415
|
-
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="margin: 0; padding: 0;">
|
|
3416
|
-
<tr>
|
|
3417
|
-
<td align="center" style="padding: 20px 0;">
|
|
3418
|
-
<table role="presentation" cellpadding="0" cellspacing="0" width="600" style="margin: 0 auto; background-color: #ffffff; border-radius: 8px; overflow: hidden;">
|
|
3419
|
-
<tr>
|
|
3420
|
-
<td style="padding: 40px 30px;">
|
|
3421
|
-
${content}
|
|
3422
|
-
</td>
|
|
3423
|
-
</tr>
|
|
3424
|
-
</table>
|
|
3425
|
-
</td>
|
|
3426
|
-
</tr>
|
|
3427
|
-
</table>
|
|
3428
|
-
</body>
|
|
3429
|
-
</html>`;
|
|
3430
|
-
}
|
|
3431
|
-
|
|
3432
|
-
// src/endpoints/broadcasts/test.ts
|
|
3433
|
-
var createTestBroadcastEndpoint = (config, collectionSlug) => {
|
|
3434
|
-
return {
|
|
3435
|
-
path: `/${collectionSlug}/:id/test`,
|
|
3436
|
-
method: "post",
|
|
3437
|
-
handler: async (req) => {
|
|
3438
|
-
try {
|
|
3439
|
-
const auth = await requireAdmin(req, config);
|
|
3440
|
-
if (!auth.authorized) {
|
|
3441
|
-
return Response.json({
|
|
3442
|
-
success: false,
|
|
3443
|
-
error: auth.error
|
|
3444
|
-
}, { status: 401 });
|
|
3445
|
-
}
|
|
3446
|
-
const url = new URL(req.url || "", `http://localhost`);
|
|
3447
|
-
const pathParts = url.pathname.split("/");
|
|
3448
|
-
const id = pathParts[pathParts.length - 2];
|
|
3449
|
-
if (!id) {
|
|
3450
|
-
return Response.json({
|
|
3451
|
-
success: false,
|
|
3452
|
-
error: "Broadcast ID is required"
|
|
3453
|
-
}, { status: 400 });
|
|
3454
|
-
}
|
|
3455
|
-
const data = await (req.json?.() || Promise.resolve({}));
|
|
3456
|
-
const testEmail = data.email || auth.user.email;
|
|
3457
|
-
if (!testEmail) {
|
|
3458
|
-
return Response.json({
|
|
3459
|
-
success: false,
|
|
3460
|
-
error: "No email address available for test send"
|
|
3461
|
-
}, { status: 400 });
|
|
3462
|
-
}
|
|
3463
|
-
const broadcast = await req.payload.findByID({
|
|
3464
|
-
collection: collectionSlug,
|
|
3465
|
-
id,
|
|
3466
|
-
user: auth.user
|
|
3467
|
-
});
|
|
3468
|
-
if (!broadcast) {
|
|
3469
|
-
return Response.json({
|
|
3470
|
-
success: false,
|
|
3471
|
-
error: "Broadcast not found"
|
|
3472
|
-
}, { status: 404 });
|
|
3473
|
-
}
|
|
3474
|
-
const htmlContent = await convertToEmailSafeHtml(broadcast.content, {
|
|
3475
|
-
wrapInTemplate: true,
|
|
3476
|
-
preheader: broadcast.preheader,
|
|
3477
|
-
customBlockConverter: config.customizations?.broadcasts?.customBlockConverter
|
|
3478
|
-
});
|
|
3479
|
-
const emailService = req.payload.newsletterEmailService;
|
|
3480
|
-
if (!emailService) {
|
|
3481
|
-
return Response.json({
|
|
3482
|
-
success: false,
|
|
3483
|
-
error: "Email service is not configured"
|
|
3484
|
-
}, { status: 500 });
|
|
3485
|
-
}
|
|
3486
|
-
const providerConfig = config.providers.default === "resend" ? config.providers.resend : config.providers.broadcast;
|
|
3487
|
-
const fromEmail = providerConfig?.fromAddress || providerConfig?.fromEmail || "noreply@example.com";
|
|
3488
|
-
const fromName = providerConfig?.fromName || "Newsletter";
|
|
3489
|
-
const replyTo = broadcast.settings?.replyTo || providerConfig?.replyTo;
|
|
3490
|
-
await emailService.send({
|
|
3491
|
-
to: testEmail,
|
|
3492
|
-
from: fromEmail,
|
|
3493
|
-
fromName,
|
|
3494
|
-
replyTo,
|
|
3495
|
-
subject: `[TEST] ${broadcast.subject}`,
|
|
3496
|
-
html: htmlContent,
|
|
3497
|
-
trackOpens: false,
|
|
3498
|
-
trackClicks: false
|
|
3499
|
-
});
|
|
3500
|
-
return Response.json({
|
|
3501
|
-
success: true,
|
|
3502
|
-
message: `Test email sent to ${testEmail}`
|
|
3503
|
-
});
|
|
3504
|
-
} catch (error) {
|
|
3505
|
-
console.error("Failed to send test broadcast:", error);
|
|
3506
|
-
return Response.json({
|
|
3507
|
-
success: false,
|
|
3508
|
-
error: "Failed to send test email"
|
|
3509
|
-
}, { status: 500 });
|
|
3510
|
-
}
|
|
3511
|
-
}
|
|
3512
|
-
};
|
|
3513
|
-
};
|
|
3514
|
-
|
|
3515
|
-
// src/endpoints/broadcasts/preview.ts
|
|
3516
|
-
var createBroadcastPreviewEndpoint = (config, collectionSlug) => {
|
|
3517
|
-
return {
|
|
3518
|
-
path: `/${collectionSlug}/preview`,
|
|
3519
|
-
method: "post",
|
|
3520
|
-
handler: async (req) => {
|
|
3521
|
-
try {
|
|
3522
|
-
const data = await (req.json?.() || Promise.resolve({}));
|
|
3523
|
-
const { content, preheader, subject } = data;
|
|
3524
|
-
if (!content) {
|
|
3525
|
-
return Response.json({
|
|
3526
|
-
success: false,
|
|
3527
|
-
error: "Content is required for preview"
|
|
3528
|
-
}, { status: 400 });
|
|
3529
|
-
}
|
|
3530
|
-
const mediaUrl = req.payload.config.serverURL ? `${req.payload.config.serverURL}/api/media` : "/api/media";
|
|
3531
|
-
const htmlContent = await convertToEmailSafeHtml(content, {
|
|
3532
|
-
wrapInTemplate: true,
|
|
3533
|
-
preheader,
|
|
3534
|
-
mediaUrl,
|
|
3535
|
-
customBlockConverter: config.customizations?.broadcasts?.customBlockConverter
|
|
3536
|
-
});
|
|
3537
|
-
return Response.json({
|
|
3538
|
-
success: true,
|
|
3539
|
-
preview: {
|
|
3540
|
-
subject: subject || "Preview",
|
|
3541
|
-
preheader: preheader || "",
|
|
3542
|
-
html: htmlContent
|
|
3543
|
-
}
|
|
3544
|
-
});
|
|
3545
|
-
} catch (error) {
|
|
3546
|
-
console.error("Failed to generate email preview:", error);
|
|
3547
|
-
return Response.json({
|
|
3548
|
-
success: false,
|
|
3549
|
-
error: "Failed to generate email preview"
|
|
3550
|
-
}, { status: 500 });
|
|
3551
|
-
}
|
|
3552
|
-
}
|
|
3773
|
+
<a href="${escapeHtml(url)}" target="_blank" rel="noopener noreferrer" style="${buttonStyle}">${escapeHtml(text)}</a>
|
|
3774
|
+
</div>
|
|
3775
|
+
`;
|
|
3776
|
+
}
|
|
3777
|
+
function convertDividerBlock(fields) {
|
|
3778
|
+
const style = fields?.style || "solid";
|
|
3779
|
+
const styles2 = {
|
|
3780
|
+
solid: "border-top: 1px solid #e5e7eb;",
|
|
3781
|
+
dashed: "border-top: 1px dashed #e5e7eb;",
|
|
3782
|
+
dotted: "border-top: 1px dotted #e5e7eb;"
|
|
3553
3783
|
};
|
|
3554
|
-
};
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
if (
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3784
|
+
return `<hr style="${styles2[style] || styles2.solid} margin: 24px 0; border-bottom: none; border-left: none; border-right: none;" />`;
|
|
3785
|
+
}
|
|
3786
|
+
function getAlignment(format) {
|
|
3787
|
+
if (!format) return "left";
|
|
3788
|
+
if (format & 2) return "center";
|
|
3789
|
+
if (format & 3) return "right";
|
|
3790
|
+
if (format & 4) return "justify";
|
|
3791
|
+
return "left";
|
|
3792
|
+
}
|
|
3793
|
+
function escapeHtml(text) {
|
|
3794
|
+
const map = {
|
|
3795
|
+
"&": "&",
|
|
3796
|
+
"<": "<",
|
|
3797
|
+
">": ">",
|
|
3798
|
+
'"': """,
|
|
3799
|
+
"'": "'"
|
|
3800
|
+
};
|
|
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
|
+
}
|
|
3569
3838
|
|
|
3570
|
-
// src/
|
|
3571
|
-
function
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
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
|
+
};
|
|
3854
|
+
}
|
|
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;
|
|
3585
3859
|
}
|
|
3586
|
-
endpoints.push(...createBroadcastManagementEndpoints(config));
|
|
3587
|
-
return endpoints;
|
|
3588
3860
|
}
|
|
3589
3861
|
|
|
3590
|
-
// src/
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
{
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
// Only show for admin users
|
|
3603
|
-
},
|
|
3604
|
-
fields: [
|
|
3605
|
-
{
|
|
3606
|
-
name: "scheduled",
|
|
3607
|
-
type: "checkbox",
|
|
3608
|
-
label: "Schedule for Newsletter",
|
|
3609
|
-
defaultValue: false,
|
|
3610
|
-
admin: {
|
|
3611
|
-
description: "Schedule this content to be sent as a newsletter"
|
|
3612
|
-
}
|
|
3613
|
-
},
|
|
3614
|
-
{
|
|
3615
|
-
name: "scheduledDate",
|
|
3616
|
-
type: "date",
|
|
3617
|
-
label: "Send Date",
|
|
3618
|
-
required: true,
|
|
3619
|
-
admin: {
|
|
3620
|
-
date: {
|
|
3621
|
-
pickerAppearance: "dayAndTime"
|
|
3622
|
-
},
|
|
3623
|
-
condition: (data) => data?.[groupName]?.scheduled,
|
|
3624
|
-
description: "When to send this newsletter"
|
|
3625
|
-
}
|
|
3626
|
-
},
|
|
3627
|
-
{
|
|
3628
|
-
name: "sentDate",
|
|
3629
|
-
type: "date",
|
|
3630
|
-
label: "Sent Date",
|
|
3631
|
-
admin: {
|
|
3632
|
-
readOnly: true,
|
|
3633
|
-
condition: (data) => data?.[groupName]?.sendStatus === "sent",
|
|
3634
|
-
description: "When this newsletter was sent"
|
|
3635
|
-
}
|
|
3636
|
-
},
|
|
3637
|
-
{
|
|
3638
|
-
name: "sendStatus",
|
|
3639
|
-
type: "select",
|
|
3640
|
-
label: "Status",
|
|
3641
|
-
options: [
|
|
3642
|
-
{ label: "Draft", value: "draft" },
|
|
3643
|
-
{ label: "Scheduled", value: "scheduled" },
|
|
3644
|
-
{ label: "Sending", value: "sending" },
|
|
3645
|
-
{ label: "Sent", value: "sent" },
|
|
3646
|
-
{ label: "Failed", value: "failed" }
|
|
3647
|
-
],
|
|
3648
|
-
defaultValue: "draft",
|
|
3649
|
-
admin: {
|
|
3650
|
-
readOnly: true,
|
|
3651
|
-
description: "Current send status"
|
|
3652
|
-
}
|
|
3653
|
-
},
|
|
3654
|
-
{
|
|
3655
|
-
name: "emailSubject",
|
|
3656
|
-
type: "text",
|
|
3657
|
-
label: "Email Subject",
|
|
3658
|
-
required: true,
|
|
3659
|
-
admin: {
|
|
3660
|
-
condition: (data) => data?.[groupName]?.scheduled,
|
|
3661
|
-
description: "Subject line for the newsletter email"
|
|
3662
|
-
}
|
|
3663
|
-
},
|
|
3664
|
-
{
|
|
3665
|
-
name: "preheader",
|
|
3666
|
-
type: "text",
|
|
3667
|
-
label: "Email Preheader",
|
|
3668
|
-
admin: {
|
|
3669
|
-
condition: (data) => data?.[groupName]?.scheduled,
|
|
3670
|
-
description: "Preview text that appears after the subject line"
|
|
3671
|
-
}
|
|
3672
|
-
},
|
|
3673
|
-
{
|
|
3674
|
-
name: "segments",
|
|
3675
|
-
type: "select",
|
|
3676
|
-
label: "Target Segments",
|
|
3677
|
-
hasMany: true,
|
|
3678
|
-
options: [
|
|
3679
|
-
{ label: "All Subscribers", value: "all" },
|
|
3680
|
-
...config.i18n?.locales?.map((locale) => ({
|
|
3681
|
-
label: `${locale.toUpperCase()} Subscribers`,
|
|
3682
|
-
value: locale
|
|
3683
|
-
})) || []
|
|
3684
|
-
],
|
|
3685
|
-
defaultValue: ["all"],
|
|
3686
|
-
admin: {
|
|
3687
|
-
condition: (data) => data?.[groupName]?.scheduled,
|
|
3688
|
-
description: "Which subscriber segments to send to"
|
|
3689
|
-
}
|
|
3690
|
-
},
|
|
3691
|
-
{
|
|
3692
|
-
name: "testEmails",
|
|
3693
|
-
type: "array",
|
|
3694
|
-
label: "Test Email Recipients",
|
|
3695
|
-
admin: {
|
|
3696
|
-
condition: (data) => data?.[groupName]?.scheduled && data?.[groupName]?.sendStatus === "draft",
|
|
3697
|
-
description: "Send test emails before scheduling"
|
|
3698
|
-
},
|
|
3699
|
-
fields: [
|
|
3700
|
-
{
|
|
3701
|
-
name: "email",
|
|
3702
|
-
type: "email",
|
|
3703
|
-
required: true
|
|
3704
|
-
}
|
|
3705
|
-
]
|
|
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
|
|
3706
3874
|
}
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
label: "Email Content (Markdown)",
|
|
3715
|
-
admin: {
|
|
3716
|
-
position: "sidebar",
|
|
3717
|
-
condition: (data) => Boolean(data?.[contentField] && data?.[groupName]?.scheduled),
|
|
3718
|
-
description: "Markdown version for email rendering",
|
|
3719
|
-
readOnly: true
|
|
3720
|
-
}
|
|
3721
|
-
}));
|
|
3875
|
+
},
|
|
3876
|
+
limit: 1,
|
|
3877
|
+
depth: 0
|
|
3878
|
+
});
|
|
3879
|
+
return me.docs[0] || null;
|
|
3880
|
+
} catch {
|
|
3881
|
+
return null;
|
|
3722
3882
|
}
|
|
3723
|
-
return fields;
|
|
3724
3883
|
}
|
|
3725
|
-
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
|
+
}
|
|
3726
3898
|
return {
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
label: config.label || "Markdown",
|
|
3730
|
-
admin: {
|
|
3731
|
-
...config.admin,
|
|
3732
|
-
description: config.admin?.description || "Auto-generated from rich text content"
|
|
3733
|
-
},
|
|
3734
|
-
hooks: {
|
|
3735
|
-
afterRead: [
|
|
3736
|
-
async ({ data }) => {
|
|
3737
|
-
if (data?.[config.richTextField]) {
|
|
3738
|
-
try {
|
|
3739
|
-
const { convertLexicalToMarkdown } = await import("@payloadcms/richtext-lexical");
|
|
3740
|
-
return convertLexicalToMarkdown({
|
|
3741
|
-
data: data[config.richTextField]
|
|
3742
|
-
});
|
|
3743
|
-
} catch {
|
|
3744
|
-
return "";
|
|
3745
|
-
}
|
|
3746
|
-
}
|
|
3747
|
-
return "";
|
|
3748
|
-
}
|
|
3749
|
-
],
|
|
3750
|
-
beforeChange: [
|
|
3751
|
-
() => {
|
|
3752
|
-
return null;
|
|
3753
|
-
}
|
|
3754
|
-
]
|
|
3755
|
-
}
|
|
3899
|
+
authorized: true,
|
|
3900
|
+
user
|
|
3756
3901
|
};
|
|
3757
3902
|
}
|
|
3758
3903
|
|
|
3759
|
-
// src/
|
|
3760
|
-
var
|
|
3904
|
+
// src/endpoints/broadcasts/send.ts
|
|
3905
|
+
var createSendBroadcastEndpoint = (config, collectionSlug) => {
|
|
3761
3906
|
return {
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
handler: async (
|
|
3765
|
-
const subscribersSlug = pluginConfig.subscribersSlug || "subscribers";
|
|
3766
|
-
const emailService = req.payload.newsletterEmailService;
|
|
3767
|
-
if (!emailService) {
|
|
3768
|
-
console.error("Email service not configured");
|
|
3769
|
-
return {
|
|
3770
|
-
output: {
|
|
3771
|
-
syncedCount: 0
|
|
3772
|
-
}
|
|
3773
|
-
};
|
|
3774
|
-
}
|
|
3775
|
-
let syncedCount = 0;
|
|
3907
|
+
path: "/:id/send",
|
|
3908
|
+
method: "post",
|
|
3909
|
+
handler: async (req) => {
|
|
3776
3910
|
try {
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
}
|
|
3783
|
-
const apiUrl = broadcastConfig.apiUrl.replace(/\/$/, "");
|
|
3784
|
-
const token = broadcastConfig.token;
|
|
3785
|
-
let page = 1;
|
|
3786
|
-
let hasMore = true;
|
|
3787
|
-
while (hasMore) {
|
|
3788
|
-
const response = await fetch(
|
|
3789
|
-
`${apiUrl}/api/v1/subscribers.json?page=${page}`,
|
|
3790
|
-
{
|
|
3791
|
-
headers: {
|
|
3792
|
-
"Authorization": `Bearer ${token}`
|
|
3793
|
-
}
|
|
3794
|
-
}
|
|
3795
|
-
);
|
|
3796
|
-
if (!response.ok) {
|
|
3797
|
-
throw new Error(`Broadcast API error: ${response.status}`);
|
|
3798
|
-
}
|
|
3799
|
-
const data = await response.json();
|
|
3800
|
-
const broadcastSubscribers = data.subscribers || [];
|
|
3801
|
-
for (const broadcastSub of broadcastSubscribers) {
|
|
3802
|
-
const payloadSubscribers = await req.payload.find({
|
|
3803
|
-
collection: subscribersSlug,
|
|
3804
|
-
where: {
|
|
3805
|
-
email: {
|
|
3806
|
-
equals: broadcastSub.email
|
|
3807
|
-
}
|
|
3808
|
-
},
|
|
3809
|
-
limit: 1
|
|
3810
|
-
});
|
|
3811
|
-
if (payloadSubscribers.docs.length > 0) {
|
|
3812
|
-
const payloadSub = payloadSubscribers.docs[0];
|
|
3813
|
-
const broadcastUnsubscribed = !broadcastSub.is_active || broadcastSub.unsubscribed_at;
|
|
3814
|
-
const payloadUnsubscribed = payloadSub.subscriptionStatus === "unsubscribed";
|
|
3815
|
-
if (broadcastUnsubscribed && !payloadUnsubscribed) {
|
|
3816
|
-
await req.payload.update({
|
|
3817
|
-
collection: subscribersSlug,
|
|
3818
|
-
id: payloadSub.id,
|
|
3819
|
-
data: {
|
|
3820
|
-
subscriptionStatus: "unsubscribed",
|
|
3821
|
-
unsubscribedAt: broadcastSub.unsubscribed_at || (/* @__PURE__ */ new Date()).toISOString()
|
|
3822
|
-
}
|
|
3823
|
-
});
|
|
3824
|
-
syncedCount++;
|
|
3825
|
-
console.warn(`Unsubscribed: ${broadcastSub.email}`);
|
|
3826
|
-
}
|
|
3827
|
-
}
|
|
3828
|
-
}
|
|
3829
|
-
if (data.pagination && data.pagination.current < data.pagination.total_pages) {
|
|
3830
|
-
page++;
|
|
3831
|
-
} else {
|
|
3832
|
-
hasMore = false;
|
|
3833
|
-
}
|
|
3834
|
-
}
|
|
3835
|
-
console.warn(`Broadcast sync complete. Unsubscribed ${syncedCount} contacts.`);
|
|
3836
|
-
}
|
|
3837
|
-
if (emailService.getProvider() === "resend") {
|
|
3838
|
-
console.warn("Starting Resend unsubscribe sync...");
|
|
3839
|
-
const resendConfig = pluginConfig.providers?.resend;
|
|
3840
|
-
if (!resendConfig) {
|
|
3841
|
-
throw new Error("Resend configuration not found");
|
|
3842
|
-
}
|
|
3843
|
-
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 });
|
|
3844
3917
|
}
|
|
3845
|
-
if (
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
});
|
|
3918
|
+
if (!config.features?.newsletterManagement?.enabled) {
|
|
3919
|
+
return Response.json({
|
|
3920
|
+
success: false,
|
|
3921
|
+
error: "Broadcast management is not enabled"
|
|
3922
|
+
}, { status: 400 });
|
|
3850
3923
|
}
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
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 });
|
|
3858
3932
|
}
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
ItalicFeature,
|
|
3871
|
-
UnderlineFeature,
|
|
3872
|
-
StrikethroughFeature,
|
|
3873
|
-
LinkFeature,
|
|
3874
|
-
OrderedListFeature,
|
|
3875
|
-
UnorderedListFeature,
|
|
3876
|
-
HeadingFeature,
|
|
3877
|
-
ParagraphFeature,
|
|
3878
|
-
AlignFeature,
|
|
3879
|
-
BlockquoteFeature,
|
|
3880
|
-
BlocksFeature,
|
|
3881
|
-
UploadFeature,
|
|
3882
|
-
FixedToolbarFeature,
|
|
3883
|
-
InlineToolbarFeature,
|
|
3884
|
-
lexicalEditor
|
|
3885
|
-
} from "@payloadcms/richtext-lexical";
|
|
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
|
-
FixedToolbarFeature(),
|
|
4050
|
-
// Fixed toolbar at the top
|
|
4051
|
-
InlineToolbarFeature(),
|
|
4052
|
-
// Floating toolbar when text is selected
|
|
4053
|
-
// Basic text formatting
|
|
4054
|
-
BoldFeature(),
|
|
4055
|
-
ItalicFeature(),
|
|
4056
|
-
UnderlineFeature(),
|
|
4057
|
-
StrikethroughFeature(),
|
|
4058
|
-
// Links with enhanced configuration
|
|
4059
|
-
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
|
-
BoldFeature(),
|
|
4129
|
-
ItalicFeature(),
|
|
4130
|
-
UnderlineFeature(),
|
|
4131
|
-
StrikethroughFeature(),
|
|
4132
|
-
// Links with enhanced configuration
|
|
4133
|
-
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
|
-
ParagraphFeature(),
|
|
4160
|
-
AlignFeature(),
|
|
4161
|
-
// Blockquotes
|
|
4162
|
-
BlockquoteFeature(),
|
|
4163
|
-
// Upload feature for images
|
|
4164
|
-
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",
|