UNPKG

payload-plugin-newsletter

Version:

Complete newsletter management plugin for Payload CMS with subscriber management, magic link authentication, and email service integration

3,066 lines 102 kB
var __defProp = Object.defineProperty;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __esm = (fn, res) => function __init() {
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
};
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};

// src/types/newsletter.ts
var NewsletterProviderError;
var init_newsletter = __esm({
  "src/types/newsletter.ts"() {
    "use strict";
    NewsletterProviderError = class extends Error {
      constructor(message, code, provider, details) {
        super(message);
        this.code = code;
        this.provider = provider;
        this.details = details;
        this.name = "NewsletterProviderError";
      }
    };
  }
});

// src/types/broadcast.ts
var BroadcastProviderError;
var init_broadcast = __esm({
  "src/types/broadcast.ts"() {
    "use strict";
    init_newsletter();
    BroadcastProviderError = class extends Error {
      constructor(message, code, provider, details) {
        super(message);
        this.code = code;
        this.provider = provider;
        this.details = details;
        this.name = "BroadcastProviderError";
      }
    };
  }
});

// src/types/providers.ts
var BaseBroadcastProvider;
var init_providers = __esm({
  "src/types/providers.ts"() {
    "use strict";
    init_broadcast();
    init_newsletter();
    BaseBroadcastProvider = class {
      constructor(config) {
        this.config = config;
      }
      /**
       * Schedule a broadcast - default implementation throws not supported
       */
      async schedule(_id, _scheduledAt) {
        const capabilities = this.getCapabilities();
        if (!capabilities.supportsScheduling) {
          throw new BroadcastProviderError(
            "Scheduling is not supported by this provider",
            "NOT_SUPPORTED" /* NOT_SUPPORTED */,
            this.name
          );
        }
        throw new Error("Method not implemented");
      }
      /**
       * Cancel scheduled broadcast - default implementation throws not supported
       */
      async cancelSchedule(_id) {
        const capabilities = this.getCapabilities();
        if (!capabilities.supportsScheduling) {
          throw new BroadcastProviderError(
            "Scheduling is not supported by this provider",
            "NOT_SUPPORTED" /* NOT_SUPPORTED */,
            this.name
          );
        }
        throw new Error("Method not implemented");
      }
      /**
       * Get analytics - default implementation returns zeros
       */
      async getAnalytics(_id) {
        const capabilities = this.getCapabilities();
        if (!capabilities.supportsAnalytics) {
          throw new BroadcastProviderError(
            "Analytics are not supported by this provider",
            "NOT_SUPPORTED" /* NOT_SUPPORTED */,
            this.name
          );
        }
        return {
          sent: 0,
          delivered: 0,
          opened: 0,
          clicked: 0,
          bounced: 0,
          complained: 0,
          unsubscribed: 0
        };
      }
      /**
       * Helper method to validate required fields
       */
      validateRequiredFields(data, fields) {
        const missing = fields.filter((field) => !data[field]);
        if (missing.length > 0) {
          throw new BroadcastProviderError(
            `Missing required fields: ${missing.join(", ")}`,
            "VALIDATION_ERROR" /* VALIDATION_ERROR */,
            this.name
          );
        }
      }
      /**
       * Helper method to check if a status transition is allowed
       */
      canEditInStatus(status) {
        const capabilities = this.getCapabilities();
        return capabilities.editableStatuses.includes(status);
      }
      /**
       * Helper to build pagination response
       */
      buildListResponse(items, total, options = {}) {
        const limit = options.limit || 20;
        const offset = options.offset || 0;
        return {
          items,
          total,
          limit,
          offset,
          hasMore: offset + items.length < total
        };
      }
    };
  }
});

// src/types/index.ts
var init_types = __esm({
  "src/types/index.ts"() {
    "use strict";
    init_broadcast();
    init_providers();
    init_newsletter();
  }
});

// src/providers/broadcast/broadcast.ts
var broadcast_exports = {};
__export(broadcast_exports, {
  BroadcastApiProvider: () => BroadcastApiProvider
});
var BroadcastApiProvider;
var init_broadcast2 = __esm({
  "src/providers/broadcast/broadcast.ts"() {
    "use strict";
    init_types();
    BroadcastApiProvider = class extends BaseBroadcastProvider {
      constructor(config) {
        super(config);
        this.name = "broadcast";
        this.apiUrl = config.apiUrl.replace(/\/$/, "");
        this.token = config.token;
        if (!this.token) {
          throw new BroadcastProviderError(
            "Broadcast API token is required",
            "CONFIGURATION_ERROR" /* CONFIGURATION_ERROR */,
            this.name
          );
        }
      }
      // Broadcast Management Methods
      async list(options) {
        try {
          const params = new URLSearchParams();
          if (options?.limit) params.append("limit", options.limit.toString());
          if (options?.offset) params.append("offset", options.offset.toString());
          const response = await fetch(`${this.apiUrl}/api/v1/broadcasts?${params}`, {
            method: "GET",
            headers: {
              "Authorization": `Bearer ${this.token}`,
              "Content-Type": "application/json"
            }
          });
          if (!response.ok) {
            const error = await response.text();
            throw new Error(`Broadcast API error: ${response.status} - ${error}`);
          }
          const data = await response.json();
          const broadcasts = data.data.map((broadcast) => this.transformBroadcastFromApi(broadcast));
          return this.buildListResponse(broadcasts, data.total, options);
        } catch (error) {
          throw new BroadcastProviderError(
            `Failed to list broadcasts: ${error instanceof Error ? error.message : "Unknown error"}`,
            "PROVIDER_ERROR" /* PROVIDER_ERROR */,
            this.name,
            error
          );
        }
      }
      async get(id) {
        try {
          const response = await fetch(`${this.apiUrl}/api/v1/broadcasts/${id}`, {
            method: "GET",
            headers: {
              "Authorization": `Bearer ${this.token}`,
              "Content-Type": "application/json"
            }
          });
          if (!response.ok) {
            if (response.status === 404) {
              throw new BroadcastProviderError(
                `Broadcast not found: ${id}`,
                "NOT_FOUND" /* NOT_FOUND */,
                this.name
              );
            }
            const error = await response.text();
            throw new Error(`Broadcast API error: ${response.status} - ${error}`);
          }
          const broadcast = await response.json();
          return this.transformBroadcastFromApi(broadcast);
        } catch (error) {
          if (error instanceof BroadcastProviderError) throw error;
          throw new BroadcastProviderError(
            `Failed to get broadcast: ${error instanceof Error ? error.message : "Unknown error"}`,
            "PROVIDER_ERROR" /* PROVIDER_ERROR */,
            this.name,
            error
          );
        }
      }
      async create(data) {
        try {
          this.validateRequiredFields(data, ["name", "subject", "content"]);
          const requestBody = {
            broadcast: {
              name: data.name,
              subject: data.subject,
              preheader: data.preheader,
              body: data.content,
              html_body: true,
              track_opens: data.trackOpens ?? true,
              track_clicks: data.trackClicks ?? true,
              reply_to: data.replyTo,
              segment_ids: data.audienceIds
            }
          };
          console.log("[BroadcastApiProvider] Creating broadcast:", {
            url: `${this.apiUrl}/api/v1/broadcasts`,
            method: "POST",
            hasToken: !!this.token,
            tokenLength: this.token?.length,
            body: JSON.stringify(requestBody, null, 2)
          });
          const response = await fetch(`${this.apiUrl}/api/v1/broadcasts`, {
            method: "POST",
            headers: {
              "Authorization": `Bearer ${this.token}`,
              "Content-Type": "application/json"
            },
            body: JSON.stringify(requestBody)
          });
          console.log("[BroadcastApiProvider] Response status:", response.status);
          console.log("[BroadcastApiProvider] Response headers:", Object.fromEntries(response.headers.entries()));
          if (!response.ok) {
            const errorText = await response.text();
            console.error("[BroadcastApiProvider] Error response body:", errorText);
            let errorDetails;
            try {
              errorDetails = JSON.parse(errorText);
              console.error("[BroadcastApiProvider] Parsed error:", errorDetails);
            } catch {
            }
            throw new Error(`Broadcast API error: ${response.status} - ${errorText}`);
          }
          const result = await response.json();
          return this.get(result.id.toString());
        } catch (error) {
          if (error instanceof BroadcastProviderError) throw error;
          throw new BroadcastProviderError(
            `Failed to create broadcast: ${error instanceof Error ? error.message : "Unknown error"}`,
            "PROVIDER_ERROR" /* PROVIDER_ERROR */,
            this.name,
            error
          );
        }
      }
      async update(id, data) {
        try {
          const existing = await this.get(id);
          if (!this.canEditInStatus(existing.sendStatus)) {
            throw new BroadcastProviderError(
              `Cannot update broadcast in status: ${existing.sendStatus}`,
              "INVALID_STATUS" /* INVALID_STATUS */,
              this.name
            );
          }
          const response = await fetch(`${this.apiUrl}/api/v1/broadcasts/${id}`, {
            method: "PATCH",
            headers: {
              "Authorization": `Bearer ${this.token}`,
              "Content-Type": "application/json"
            },
            body: JSON.stringify({
              broadcast: {
                name: data.name,
                subject: data.subject,
                preheader: data.preheader,
                body: data.content,
                track_opens: data.trackOpens,
                track_clicks: data.trackClicks,
                reply_to: data.replyTo,
                segment_ids: data.audienceIds
              }
            })
          });
          if (!response.ok) {
            const error = await response.text();
            throw new Error(`Broadcast API error: ${response.status} - ${error}`);
          }
          const broadcast = await response.json();
          return this.transformBroadcastFromApi(broadcast);
        } catch (error) {
          if (error instanceof BroadcastProviderError) throw error;
          throw new BroadcastProviderError(
            `Failed to update broadcast: ${error instanceof Error ? error.message : "Unknown error"}`,
            "PROVIDER_ERROR" /* PROVIDER_ERROR */,
            this.name,
            error
          );
        }
      }
      async delete(id) {
        try {
          const existing = await this.get(id);
          if (!this.canEditInStatus(existing.sendStatus)) {
            throw new BroadcastProviderError(
              `Cannot delete broadcast in status: ${existing.sendStatus}`,
              "INVALID_STATUS" /* INVALID_STATUS */,
              this.name
            );
          }
          const response = await fetch(`${this.apiUrl}/api/v1/broadcasts/${id}`, {
            method: "DELETE",
            headers: {
              "Authorization": `Bearer ${this.token}`,
              "Content-Type": "application/json"
            }
          });
          if (!response.ok) {
            const error = await response.text();
            throw new Error(`Broadcast API error: ${response.status} - ${error}`);
          }
        } catch (error) {
          if (error instanceof BroadcastProviderError) throw error;
          throw new BroadcastProviderError(
            `Failed to delete broadcast: ${error instanceof Error ? error.message : "Unknown error"}`,
            "PROVIDER_ERROR" /* PROVIDER_ERROR */,
            this.name,
            error
          );
        }
      }
      async send(id, options) {
        try {
          if (options?.testMode && options.testRecipients?.length) {
            throw new BroadcastProviderError(
              "Test send is not yet implemented for Broadcast provider",
              "NOT_SUPPORTED" /* NOT_SUPPORTED */,
              this.name
            );
          }
          const response = await fetch(`${this.apiUrl}/api/v1/broadcasts/${id}/send_broadcast`, {
            method: "POST",
            headers: {
              "Authorization": `Bearer ${this.token}`,
              "Content-Type": "application/json"
            },
            body: JSON.stringify({
              segment_ids: options?.audienceIds
            })
          });
          if (!response.ok) {
            const error = await response.text();
            throw new Error(`Broadcast API error: ${response.status} - ${error}`);
          }
          const result = await response.json();
          return this.get(result.id.toString());
        } catch (error) {
          if (error instanceof BroadcastProviderError) throw error;
          throw new BroadcastProviderError(
            `Failed to send broadcast: ${error instanceof Error ? error.message : "Unknown error"}`,
            "PROVIDER_ERROR" /* PROVIDER_ERROR */,
            this.name,
            error
          );
        }
      }
      async schedule(id, scheduledAt) {
        try {
          const response = await fetch(`${this.apiUrl}/api/v1/broadcasts/${id}`, {
            method: "PATCH",
            headers: {
              "Authorization": `Bearer ${this.token}`,
              "Content-Type": "application/json"
            },
            body: JSON.stringify({
              broadcast: {
                scheduled_send_at: scheduledAt.toISOString(),
                // TODO: Handle timezone properly
                scheduled_timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
              }
            })
          });
          if (!response.ok) {
            const error = await response.text();
            throw new Error(`Broadcast API error: ${response.status} - ${error}`);
          }
          const broadcast = await response.json();
          return this.transformBroadcastFromApi(broadcast);
        } catch (error) {
          throw new BroadcastProviderError(
            `Failed to schedule broadcast: ${error instanceof Error ? error.message : "Unknown error"}`,
            "PROVIDER_ERROR" /* PROVIDER_ERROR */,
            this.name,
            error
          );
        }
      }
      async cancelSchedule(id) {
        try {
          const response = await fetch(`${this.apiUrl}/api/v1/broadcasts/${id}`, {
            method: "PATCH",
            headers: {
              "Authorization": `Bearer ${this.token}`,
              "Content-Type": "application/json"
            },
            body: JSON.stringify({
              broadcast: {
                scheduled_send_at: null,
                scheduled_timezone: null
              }
            })
          });
          if (!response.ok) {
            const error = await response.text();
            throw new Error(`Broadcast API error: ${response.status} - ${error}`);
          }
          const broadcast = await response.json();
          return this.transformBroadcastFromApi(broadcast);
        } catch (error) {
          throw new BroadcastProviderError(
            `Failed to cancel scheduled broadcast: ${error instanceof Error ? error.message : "Unknown error"}`,
            "PROVIDER_ERROR" /* PROVIDER_ERROR */,
            this.name,
            error
          );
        }
      }
      async getAnalytics(_id) {
        throw new BroadcastProviderError(
          "Analytics API not yet implemented for Broadcast provider",
          "NOT_SUPPORTED" /* NOT_SUPPORTED */,
          this.name
        );
      }
      getCapabilities() {
        return {
          supportsScheduling: true,
          supportsSegmentation: true,
          supportsAnalytics: false,
          // Not documented yet
          supportsABTesting: false,
          supportsTemplates: false,
          supportsPersonalization: true,
          supportsMultipleChannels: false,
          supportsChannelSegmentation: false,
          editableStatuses: ["draft" /* DRAFT */, "scheduled" /* SCHEDULED */],
          supportedContentTypes: ["html", "text"]
        };
      }
      async validateConfiguration() {
        try {
          await this.list({ limit: 1 });
          return true;
        } catch {
          return false;
        }
      }
      transformBroadcastFromApi(broadcast) {
        return {
          id: broadcast.id.toString(),
          name: broadcast.name,
          subject: broadcast.subject,
          preheader: broadcast.preheader,
          content: broadcast.body,
          sendStatus: this.mapBroadcastStatus(broadcast.status),
          trackOpens: broadcast.track_opens,
          trackClicks: broadcast.track_clicks,
          replyTo: broadcast.reply_to,
          recipientCount: broadcast.total_recipients,
          sentAt: broadcast.sent_at ? new Date(broadcast.sent_at) : void 0,
          scheduledAt: broadcast.scheduled_send_at ? new Date(broadcast.scheduled_send_at) : void 0,
          createdAt: new Date(broadcast.created_at),
          updatedAt: new Date(broadcast.updated_at),
          providerData: { broadcast },
          providerId: broadcast.id.toString(),
          providerType: "broadcast"
        };
      }
      mapBroadcastStatus(status) {
        const statusMap = {
          "draft": "draft" /* DRAFT */,
          "scheduled": "scheduled" /* SCHEDULED */,
          "queueing": "sending" /* SENDING */,
          "sending": "sending" /* SENDING */,
          "sent": "sent" /* SENT */,
          "failed": "failed" /* FAILED */,
          "partial_failure": "failed" /* FAILED */,
          "paused": "paused" /* PAUSED */,
          "aborted": "canceled" /* CANCELED */
        };
        return statusMap[status] || "draft" /* DRAFT */;
      }
    };
  }
});

// src/collections/Broadcasts.ts
init_types();

// src/fields/emailContent.ts
import {
  BoldFeature,
  ItalicFeature,
  UnderlineFeature,
  StrikethroughFeature,
  LinkFeature,
  OrderedListFeature,
  UnorderedListFeature,
  HeadingFeature,
  ParagraphFeature,
  AlignFeature,
  BlockquoteFeature,
  BlocksFeature,
  UploadFeature,
  FixedToolbarFeature,
  InlineToolbarFeature,
  lexicalEditor
} from "@payloadcms/richtext-lexical";

// src/utils/blockValidation.ts
var EMAIL_INCOMPATIBLE_TYPES = [
  "chart",
  "dataTable",
  "interactive",
  "streamable",
  "video",
  "iframe",
  "form",
  "carousel",
  "tabs",
  "accordion",
  "map"
];
var validateEmailBlocks = (blocks) => {
  blocks.forEach((block) => {
    if (EMAIL_INCOMPATIBLE_TYPES.includes(block.slug)) {
      console.warn(`\u26A0\uFE0F  Block "${block.slug}" may not be email-compatible. Consider creating an email-specific version.`);
    }
    const hasComplexFields = block.fields?.some((field) => {
      const complexTypes = ["code", "json", "richText", "blocks", "array"];
      return complexTypes.includes(field.type);
    });
    if (hasComplexFields) {
      console.warn(`\u26A0\uFE0F  Block "${block.slug}" contains complex field types that may not render consistently in email clients.`);
    }
  });
};
var createEmailSafeBlocks = (customBlocks = []) => {
  validateEmailBlocks(customBlocks);
  const baseBlocks = [
    {
      slug: "button",
      fields: [
        {
          name: "text",
          type: "text",
          label: "Button Text",
          required: true
        },
        {
          name: "url",
          type: "text",
          label: "Button URL",
          required: true,
          admin: {
            description: "Enter the full URL (including https://)"
          }
        },
        {
          name: "style",
          type: "select",
          label: "Button Style",
          defaultValue: "primary",
          options: [
            { label: "Primary", value: "primary" },
            { label: "Secondary", value: "secondary" },
            { label: "Outline", value: "outline" }
          ]
        }
      ],
      interfaceName: "EmailButton",
      labels: {
        singular: "Button",
        plural: "Buttons"
      }
    },
    {
      slug: "divider",
      fields: [
        {
          name: "style",
          type: "select",
          label: "Divider Style",
          defaultValue: "solid",
          options: [
            { label: "Solid", value: "solid" },
            { label: "Dashed", value: "dashed" },
            { label: "Dotted", value: "dotted" }
          ]
        }
      ],
      interfaceName: "EmailDivider",
      labels: {
        singular: "Divider",
        plural: "Dividers"
      }
    }
  ];
  return [
    ...baseBlocks,
    ...customBlocks
  ];
};

// src/fields/emailContent.ts
var createEmailSafeFeatures = (additionalBlocks) => {
  const baseBlocks = [
    {
      slug: "button",
      fields: [
        {
          name: "text",
          type: "text",
          label: "Button Text",
          required: true
        },
        {
          name: "url",
          type: "text",
          label: "Button URL",
          required: true,
          admin: {
            description: "Enter the full URL (including https://)"
          }
        },
        {
          name: "style",
          type: "select",
          label: "Button Style",
          defaultValue: "primary",
          options: [
            { label: "Primary", value: "primary" },
            { label: "Secondary", value: "secondary" },
            { label: "Outline", value: "outline" }
          ]
        }
      ],
      interfaceName: "EmailButton",
      labels: {
        singular: "Button",
        plural: "Buttons"
      }
    },
    {
      slug: "divider",
      fields: [
        {
          name: "style",
          type: "select",
          label: "Divider Style",
          defaultValue: "solid",
          options: [
            { label: "Solid", value: "solid" },
            { label: "Dashed", value: "dashed" },
            { label: "Dotted", value: "dotted" }
          ]
        }
      ],
      interfaceName: "EmailDivider",
      labels: {
        singular: "Divider",
        plural: "Dividers"
      }
    }
  ];
  const allBlocks = [
    ...baseBlocks,
    ...additionalBlocks || []
  ];
  return [
    // Toolbars
    FixedToolbarFeature(),
    // Fixed toolbar at the top
    InlineToolbarFeature(),
    // Floating toolbar when text is selected
    // Basic text formatting
    BoldFeature(),
    ItalicFeature(),
    UnderlineFeature(),
    StrikethroughFeature(),
    // Links with enhanced configuration
    LinkFeature({
      fields: [
        {
          name: "url",
          type: "text",
          required: true,
          admin: {
            description: "Enter the full URL (including https://)"
          }
        },
        {
          name: "newTab",
          type: "checkbox",
          label: "Open in new tab",
          defaultValue: false
        }
      ]
    }),
    // Lists
    OrderedListFeature(),
    UnorderedListFeature(),
    // Headings - limited to h1, h2, h3 for email compatibility
    HeadingFeature({
      enabledHeadingSizes: ["h1", "h2", "h3"]
    }),
    // Basic paragraph and alignment
    ParagraphFeature(),
    AlignFeature(),
    // Blockquotes
    BlockquoteFeature(),
    // Upload feature for images
    UploadFeature({
      collections: {
        media: {
          fields: [
            {
              name: "caption",
              type: "text",
              admin: {
                description: "Optional caption for the image"
              }
            },
            {
              name: "altText",
              type: "text",
              label: "Alt Text",
              required: true,
              admin: {
                description: "Alternative text for accessibility and when image cannot be displayed"
              }
            }
          ]
        }
      }
    }),
    // Custom blocks for email-specific content
    BlocksFeature({
      blocks: allBlocks
    })
  ];
};
var createEmailLexicalEditor = (customBlocks = []) => {
  const emailSafeBlocks = createEmailSafeBlocks(customBlocks);
  return lexicalEditor({
    features: [
      // Toolbars
      FixedToolbarFeature(),
      InlineToolbarFeature(),
      // Basic text formatting
      BoldFeature(),
      ItalicFeature(),
      UnderlineFeature(),
      StrikethroughFeature(),
      // Links with enhanced configuration
      LinkFeature({
        fields: [
          {
            name: "url",
            type: "text",
            required: true,
            admin: {
              description: "Enter the full URL (including https://)"
            }
          },
          {
            name: "newTab",
            type: "checkbox",
            label: "Open in new tab",
            defaultValue: false
          }
        ]
      }),
      // Lists
      OrderedListFeature(),
      UnorderedListFeature(),
      // Headings - limited to h1, h2, h3 for email compatibility
      HeadingFeature({
        enabledHeadingSizes: ["h1", "h2", "h3"]
      }),
      // Basic paragraph and alignment
      ParagraphFeature(),
      AlignFeature(),
      // Blockquotes
      BlockquoteFeature(),
      // Upload feature for images
      UploadFeature({
        collections: {
          media: {
            fields: [
              {
                name: "caption",
                type: "text",
                admin: {
                  description: "Optional caption for the image"
                }
              },
              {
                name: "altText",
                type: "text",
                label: "Alt Text",
                required: true,
                admin: {
                  description: "Alternative text for accessibility and when image cannot be displayed"
                }
              }
            ]
          }
        }
      }),
      // Email-safe blocks (processed server-side)
      BlocksFeature({
        blocks: emailSafeBlocks
      })
    ]
  });
};
var emailSafeFeatures = createEmailSafeFeatures();
var createEmailContentField = (overrides) => {
  const editor = overrides?.editor || createEmailLexicalEditor(overrides?.additionalBlocks);
  return {
    name: "content",
    type: "richText",
    required: true,
    editor,
    admin: {
      description: "Email content with limited formatting for compatibility",
      ...overrides?.admin
    },
    ...overrides
  };
};

// src/fields/broadcastInlinePreview.ts
var createBroadcastInlinePreviewField = () => {
  return {
    name: "broadcastInlinePreview",
    type: "ui",
    admin: {
      components: {
        Field: "payload-plugin-newsletter/components#BroadcastInlinePreview"
      }
    }
  };
};

// src/utils/emailSafeHtml.ts
import DOMPurify from "isomorphic-dompurify";
var EMAIL_SAFE_CONFIG = {
  ALLOWED_TAGS: [
    "p",
    "br",
    "strong",
    "b",
    "em",
    "i",
    "u",
    "strike",
    "s",
    "span",
    "a",
    "h1",
    "h2",
    "h3",
    "ul",
    "ol",
    "li",
    "blockquote",
    "hr",
    "img",
    "div",
    "table",
    "tr",
    "td",
    "th",
    "tbody",
    "thead"
  ],
  ALLOWED_ATTR: ["href", "style", "target", "rel", "align", "src", "alt", "width", "height", "border", "cellpadding", "cellspacing"],
  ALLOWED_STYLES: {
    "*": [
      "color",
      "background-color",
      "font-size",
      "font-weight",
      "font-style",
      "text-decoration",
      "text-align",
      "margin",
      "margin-top",
      "margin-right",
      "margin-bottom",
      "margin-left",
      "padding",
      "padding-top",
      "padding-right",
      "padding-bottom",
      "padding-left",
      "line-height",
      "border-left",
      "border-left-width",
      "border-left-style",
      "border-left-color"
    ]
  },
  FORBID_TAGS: ["script", "style", "iframe", "object", "embed", "form", "input"],
  FORBID_ATTR: ["class", "id", "onclick", "onload", "onerror"]
};
async function convertToEmailSafeHtml(editorState, options) {
  if (!editorState) {
    return "";
  }
  const rawHtml = await lexicalToEmailHtml(editorState, options?.mediaUrl, options?.customBlockConverter);
  const sanitizedHtml = DOMPurify.sanitize(rawHtml, EMAIL_SAFE_CONFIG);
  if (options?.wrapInTemplate) {
    if (options.customWrapper) {
      return await Promise.resolve(options.customWrapper(sanitizedHtml, {
        preheader: options.preheader,
        subject: options.subject,
        documentData: options.documentData
      }));
    }
    return wrapInEmailTemplate(sanitizedHtml, options.preheader);
  }
  return sanitizedHtml;
}
async function lexicalToEmailHtml(editorState, mediaUrl, customBlockConverter) {
  const { root } = editorState;
  if (!root || !root.children) {
    return "";
  }
  const htmlParts = await Promise.all(
    root.children.map((node) => convertNode(node, mediaUrl, customBlockConverter))
  );
  return htmlParts.join("");
}
async function convertNode(node, mediaUrl, customBlockConverter) {
  switch (node.type) {
    case "paragraph":
      return convertParagraph(node, mediaUrl, customBlockConverter);
    case "heading":
      return convertHeading(node, mediaUrl, customBlockConverter);
    case "list":
      return convertList(node, mediaUrl, customBlockConverter);
    case "listitem":
      return convertListItem(node, mediaUrl, customBlockConverter);
    case "blockquote":
      return convertBlockquote(node, mediaUrl, customBlockConverter);
    case "text":
      return convertText(node);
    case "link":
      return convertLink(node, mediaUrl, customBlockConverter);
    case "linebreak":
      return "<br>";
    case "upload":
      return convertUpload(node, mediaUrl);
    case "block":
      return await convertBlock(node, mediaUrl, customBlockConverter);
    default:
      if (node.children) {
        const childParts = await Promise.all(
          node.children.map((child) => convertNode(child, mediaUrl, customBlockConverter))
        );
        return childParts.join("");
      }
      return "";
  }
}
async function convertParagraph(node, mediaUrl, customBlockConverter) {
  const align = getAlignment(node.format);
  const childParts = await Promise.all(
    (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
  );
  const children = childParts.join("");
  if (!children.trim()) {
    return '<p class="mobile-margin-bottom-16" style="margin: 0 0 16px 0; min-height: 1em;">&nbsp;</p>';
  }
  return `<p class="mobile-margin-bottom-16" style="margin: 0 0 16px 0; text-align: ${align}; font-size: 16px; line-height: 1.5;">${children}</p>`;
}
async function convertHeading(node, mediaUrl, customBlockConverter) {
  const tag = node.tag || "h1";
  const align = getAlignment(node.format);
  const childParts = await Promise.all(
    (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
  );
  const children = childParts.join("");
  const styles2 = {
    h1: "font-size: 32px; font-weight: 700; margin: 0 0 24px 0; line-height: 1.2;",
    h2: "font-size: 24px; font-weight: 600; margin: 0 0 16px 0; line-height: 1.3;",
    h3: "font-size: 20px; font-weight: 600; margin: 0 0 12px 0; line-height: 1.4;"
  };
  const mobileClasses = {
    h1: "mobile-font-size-24",
    h2: "mobile-font-size-20",
    h3: "mobile-font-size-16"
  };
  const style = `${styles2[tag] || styles2.h3} text-align: ${align};`;
  const mobileClass = mobileClasses[tag] || mobileClasses.h3;
  return `<${tag} class="${mobileClass}" style="${style}">${children}</${tag}>`;
}
async function convertList(node, mediaUrl, customBlockConverter) {
  const tag = node.listType === "number" ? "ol" : "ul";
  const childParts = await Promise.all(
    (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
  );
  const children = childParts.join("");
  const style = tag === "ul" ? "margin: 0 0 16px 0; padding-left: 24px; list-style-type: disc; font-size: 16px; line-height: 1.5;" : "margin: 0 0 16px 0; padding-left: 24px; list-style-type: decimal; font-size: 16px; line-height: 1.5;";
  return `<${tag} class="mobile-margin-bottom-16" style="${style}">${children}</${tag}>`;
}
async function convertListItem(node, mediaUrl, customBlockConverter) {
  const childParts = await Promise.all(
    (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
  );
  const children = childParts.join("");
  return `<li style="margin: 0 0 8px 0;">${children}</li>`;
}
async function convertBlockquote(node, mediaUrl, customBlockConverter) {
  const childParts = await Promise.all(
    (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
  );
  const children = childParts.join("");
  const style = "margin: 0 0 16px 0; padding-left: 16px; border-left: 4px solid #e5e7eb; color: #6b7280;";
  return `<blockquote style="${style}">${children}</blockquote>`;
}
function convertText(node) {
  let text = escapeHtml(node.text || "");
  if (node.format & 1) {
    text = `<strong>${text}</strong>`;
  }
  if (node.format & 2) {
    text = `<em>${text}</em>`;
  }
  if (node.format & 8) {
    text = `<u>${text}</u>`;
  }
  if (node.format & 4) {
    text = `<strike>${text}</strike>`;
  }
  return text;
}
async function convertLink(node, mediaUrl, customBlockConverter) {
  const childParts = await Promise.all(
    (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
  );
  const children = childParts.join("");
  const url = node.fields?.url || "#";
  const newTab = node.fields?.newTab ?? false;
  const targetAttr = newTab ? ' target="_blank"' : "";
  const relAttr = newTab ? ' rel="noopener noreferrer"' : "";
  return `<a href="${escapeHtml(url)}"${targetAttr}${relAttr} style="color: #2563eb; text-decoration: underline;">${children}</a>`;
}
function convertUpload(node, mediaUrl) {
  const upload = node.value;
  if (!upload) return "";
  let src = "";
  if (typeof upload === "string") {
    src = upload;
  } else if (upload.url) {
    src = upload.url;
  } else if (upload.filename && mediaUrl) {
    src = `${mediaUrl}/${upload.filename}`;
  }
  const alt = node.fields?.altText || upload.alt || "";
  const caption = node.fields?.caption || "";
  const imgHtml = `<img src="${escapeHtml(src)}" alt="${escapeHtml(alt)}" class="mobile-width-100" style="max-width: 100%; height: auto; display: block; margin: 0 auto; border-radius: 6px;" />`;
  if (caption) {
    return `
      <div style="margin: 0 0 16px 0; text-align: center;" class="mobile-margin-bottom-16">
        ${imgHtml}
        <p style="margin: 8px 0 0 0; font-size: 14px; color: #6b7280; font-style: italic; text-align: center;" class="mobile-font-size-14">${escapeHtml(caption)}</p>
      </div>
    `;
  }
  return `<div style="margin: 0 0 16px 0; text-align: center;" class="mobile-margin-bottom-16">${imgHtml}</div>`;
}
async function convertBlock(node, mediaUrl, customBlockConverter) {
  const blockType = node.fields?.blockName || node.blockName;
  if (customBlockConverter) {
    try {
      const customHtml = await customBlockConverter(node, mediaUrl);
      if (customHtml) {
        return customHtml;
      }
    } catch (error) {
      console.error(`Custom block converter error for ${blockType}:`, error);
    }
  }
  switch (blockType) {
    case "button":
      return convertButtonBlock(node.fields);
    case "divider":
      return convertDividerBlock(node.fields);
    default:
      if (node.children) {
        const childParts = await Promise.all(
          node.children.map((child) => convertNode(child, mediaUrl, customBlockConverter))
        );
        return childParts.join("");
      }
      return "";
  }
}
function convertButtonBlock(fields) {
  const text = fields?.text || "Click here";
  const url = fields?.url || "#";
  const style = fields?.style || "primary";
  const styles2 = {
    primary: "background-color: #2563eb; color: #ffffff; border: 2px solid #2563eb;",
    secondary: "background-color: #6b7280; color: #ffffff; border: 2px solid #6b7280;",
    outline: "background-color: transparent; color: #2563eb; border: 2px solid #2563eb;"
  };
  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;`;
  return `
    <div style="margin: 0 0 16px 0; text-align: center;">
      <a href="${escapeHtml(url)}" target="_blank" rel="noopener noreferrer" style="${buttonStyle}">${escapeHtml(text)}</a>
    </div>
  `;
}
function convertDividerBlock(fields) {
  const style = fields?.style || "solid";
  const styles2 = {
    solid: "border-top: 1px solid #e5e7eb;",
    dashed: "border-top: 1px dashed #e5e7eb;",
    dotted: "border-top: 1px dotted #e5e7eb;"
  };
  return `<hr style="${styles2[style] || styles2.solid} margin: 24px 0; border-bottom: none; border-left: none; border-right: none;" />`;
}
function getAlignment(format) {
  if (!format) return "left";
  if (format & 2) return "center";
  if (format & 3) return "right";
  if (format & 4) return "justify";
  return "left";
}
function escapeHtml(text) {
  const map = {
    "&": "&amp;",
    "<": "&lt;",
    ">": "&gt;",
    '"': "&quot;",
    "'": "&#039;"
  };
  return text.replace(/[&<>"']/g, (m) => map[m]);
}
function wrapInEmailTemplate(content, preheader) {
  return `<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="x-apple-disable-message-reformatting">
  <title>Newsletter</title>
  
  <!--[if mso]>
  <noscript>
    <xml>
      <o:OfficeDocumentSettings>
        <o:PixelsPerInch>96</o:PixelsPerInch>
      </o:OfficeDocumentSettings>
    </xml>
  </noscript>
  <![endif]-->
  
  <style>
    /* Reset and base styles */
    * {
      -webkit-text-size-adjust: 100%;
      -ms-text-size-adjust: 100%;
    }
    
    body {
      margin: 0 !important;
      padding: 0 !important;
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
      font-size: 16px;
      line-height: 1.5;
      color: #1A1A1A;
      background-color: #f8f9fa;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
    }
    
    table {
      border-spacing: 0 !important;
      border-collapse: collapse !important;
      table-layout: fixed !important;
      margin: 0 auto !important;
    }
    
    table table table {
      table-layout: auto;
    }
    
    img {
      -ms-interpolation-mode: bicubic;
      max-width: 100%;
      height: auto;
      border: 0;
      outline: none;
      text-decoration: none;
    }
    
    /* Responsive styles */
    @media only screen and (max-width: 640px) {
      .mobile-hide {
        display: none !important;
      }
      
      .mobile-center {
        text-align: center !important;
      }
      
      .mobile-width-100 {
        width: 100% !important;
        max-width: 100% !important;
      }
      
      .mobile-padding {
        padding: 20px !important;
      }
      
      .mobile-padding-sm {
        padding: 16px !important;
      }
      
      .mobile-font-size-14 {
        font-size: 14px !important;
      }
      
      .mobile-font-size-16 {
        font-size: 16px !important;
      }
      
      .mobile-font-size-20 {
        font-size: 20px !important;
        line-height: 1.3 !important;
      }
      
      .mobile-font-size-24 {
        font-size: 24px !important;
        line-height: 1.2 !important;
      }
      
      /* Stack sections on mobile */
      .mobile-stack {
        display: block !important;
        width: 100% !important;
      }
      
      /* Mobile-specific spacing */
      .mobile-margin-bottom-16 {
        margin-bottom: 16px !important;
      }
      
      .mobile-margin-bottom-20 {
        margin-bottom: 20px !important;
      }
    }
    
    /* Dark mode support */
    @media (prefers-color-scheme: dark) {
      .dark-mode-bg {
        background-color: #1a1a1a !important;
      }
      
      .dark-mode-text {
        color: #ffffff !important;
      }
      
      .dark-mode-border {
        border-color: #333333 !important;
      }
    }
    
    /* Outlook-specific fixes */
    <!--[if mso]>
    <style>
      table {
        border-collapse: collapse;
        border-spacing: 0;
        border: none;
        margin: 0;
      }
      
      div, p {
        margin: 0;
      }
    </style>
    <![endif]-->
  </style>
</head>
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.5; color: #1A1A1A; background-color: #f8f9fa;">
  ${preheader ? `
  <!-- Preheader text -->
  <div style="display: none; max-height: 0; overflow: hidden; font-size: 1px; line-height: 1px; color: transparent;">
    ${escapeHtml(preheader)}
  </div>
  ` : ""}
  
  <!-- Main container -->
  <table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="margin: 0; padding: 0; background-color: #f8f9fa;">
    <tr>
      <td align="center" style="padding: 20px 10px;">
        <!-- Email wrapper -->
        <table role="presentation" cellpadding="0" cellspacing="0" width="600" class="mobile-width-100" style="margin: 0 auto; max-width: 600px;">
          <tr>
            <td class="mobile-padding" style="padding: 0;">
              <!-- Content area with light background -->
              <div style="background-color: #ffffff; padding: 40px 30px; border-radius: 8px;" class="mobile-padding">
                ${content}
              </div>
            </td>
          </tr>
        </table>
      </td>
    </tr>
  </table>
</body>
</html>`;
}

// src/utils/getBroadcastConfig.ts
async function getBroadcastConfig(req, pluginConfig) {
  try {
    const settings = await req.payload.findGlobal({
      slug: pluginConfig.settingsSlug || "newsletter-settings",
      req
    });
    if (settings?.provider === "broadcast" && settings?.broadcastSettings) {
      return {
        apiUrl: settings.broadcastSettings.apiUrl || pluginConfig.providers?.broadcast?.apiUrl || "",
        token: settings.broadcastSettings.token || pluginConfig.providers?.broadcast?.token || "",
        fromAddress: settings.fromAddress || pluginConfig.providers?.broadcast?.fromAddress || "",
        fromName: settings.fromName || pluginConfig.providers?.broadcast?.fromName || "",
        replyTo: settings.replyTo || pluginConfig.providers?.broadcast?.replyTo
      };
    }
    return pluginConfig.providers?.broadcast || null;
  } catch (error) {
    req.payload.logger.error("Failed to get broadcast config from settings:", error);
    return pluginConfig.providers?.broadcast || null;
  }
}

// src/endpoints/broadcasts/send.ts
init_types();

// src/utils/access.ts
var isAdmin = (user, config) => {
  if (!user || user.collection !== "users") {
    return false;
  }
  if (config?.access?.isAdmin) {
    return config.access.isAdmin(user);
  }
  if (user.roles?.includes("admin")) {
    return true;
  }
  if (user.isAdmin === true) {
    return true;
  }
  if (user.role === "admin") {
    return true;
  }
  if (user.admin === true) {
    return true;
  }
  return false;
};
var adminOnly = (config) => ({ req }) => {
  const user = req.user;
  return isAdmin(user, config);
};
var adminOrSelf = (config) => ({ req, id }) => {
  const user = req.user;
  if (!user) {
    if (!id) {
      return {
        id: {
          equals: "unauthorized-no-access"
        }
      };
    }
    return false;
  }
  if (isAdmin(user, config)) {
    return true;
  }
  if (user.collection === "subscribers") {
    if (!id) {
      return {
        id: {
          equals: user.id
        }
      };
    }
    return id === user.id;
  }
  if (!id) {
    return {
      id: {
        equals: "unauthorized-no-access"
      }
    };
  }
  return false;
};

// src/utils/auth.ts
async function getAuthenticatedUser(req) {
  try {
    const me = await req.payload.find({
      collection: "users",
      where: {
        id: {
          equals: "me"
          // Special value in Payload to get current user
        }
      },
      limit: 1,
      depth: 0
    });
    return me.docs[0] || null;
  } catch {
    return null;
  }
}
async function requireAdmin(req, config) {
  const user = await getAuthenticatedUser(req);
  if (!user) {
    return {
      authorized: false,
      error: "Authentication required"
    };
  }
  if (!isAdmin(user, config)) {
    return {
      authorized: false,
      error: "Admin access required"
    };
  }
  return {
    authorized: true,
    user
  };
}

// src/endpoints/broadcasts/send.ts
var createSendBroadcastEndpoint = (config, collectionSlug) => {
  return {
    path: "/:id/send",
    method: "post",
    handler: async (req) => {
      try {
        const auth = await requireAdmin(req, config);
        if (!auth.authorized) {
          return Response.json({
            success: false,
            error: auth.error
          }, { status: 401 });
        }
        if (!config.features?.newsletterManagement?.enabled) {
          return Response.json({
            success: false,
            error: "Broadcast management is not enabled"
          }, { status: 400 });
        }
        const url = new URL(req.url || "", `http://localhost`);
        const pathParts = url.pathname.split("/");
        const id = pathParts[pathParts.length - 2];
        if (!id) {
          return Response.json({
            success: false,
            error: "Broadcast ID is required"
          }, { status: 400 });
        }
        const data = await (req.json?.() || Promise.resolve({}));
        const broadcastDoc = await req.payload.findByID({
          collection: collectionSlug,
          id,
          user: auth.user
        });
        if (!broadcastDoc || !broadcastDoc.providerId) {
          return Response.json({
            success: false,
            error: "Broadcast not found or not synced with provider"
          }, { status: 404 });
        }
        const providerConfig = await getBroadcastConfig(req, config);
        if (!providerConfig || !providerConfig.token) {
          return Response.json({
            success: false,
            error: "Broadcast provider not configured in Newsletter Settings or environment variables"
          }, { status: 500 });
        }
        const { BroadcastApiProvider: BroadcastApiProvider2 } = await Promise.resolve().then(() => (init_broadcast2(), broadcast_exports));
        const provider = new BroadcastApiProvider2(providerConfig);
        const broadcast = await provider.send(broadcastDoc.providerId, data);
        await req.payload.update({
          collection: collectionSlug,
          id,
          data: {
            sendStatus: "sending" /* SENDING */,
            sentAt: (/* @__PURE__ */ new Date()).toISOString()
          },
          user: auth.user
        });
        return Response.json({
          success: true,
          message: "Broadcast sent successfully",
          broadcast
        });
      } catch (error) {
        console.error("Failed to send broadcast:", error);
        if (error instanceof NewsletterProviderError) {
          return Response.json({
            success: false,
            error: error.message,
            code: error.code
          }, { status: error.code === "NOT_SUPPORTED" ? 501 : 500 });
        }
        return Response.json({
          success: false,
          error: "Failed to send broadcast"
        }, { status: 500 });
      }
    }
  };
};

// src/endpoints/broadcasts/schedule.ts
init_types();
var createScheduleBroadcastEndpoint = (config, collectionSlug) => {
  return {
    path: "/:id/schedule",
    method: "post",
    handler: async (req) => {
      try {
        const auth = await requireAdmin(req, config);
        if (!auth.authorized) {
          return Response.json({
            success: false,
            error: auth.error
          }, { status: 401 });
        }
        if (!config.features?.newsletterManagement?.enabled) {
          return Response.json({
            success: false,
            error: "Broadcast management is not enabled"
          }, { status: 400 });
        }
        const url = new URL(req.url || "", `http://localhost`);
        const pathParts = url.pathname.split("/");
        const id = pathParts[pathParts.length - 2];
        if (!id) {
          return Response.json({
            success: false,
            error: "Broadcast ID is required"
          }, { status: 400 });
        }
        const data = await (req.json?.() || Promise.resolve({}));
        const { scheduledAt } = data;
        if (!scheduledAt) {
          return Response.json({
            success: false,
            error: "scheduledAt is required"
          }, { status: 400 });
        }
        const scheduledDate = new Date(scheduledAt);
        if (isNaN(scheduledDate.getTime())) {
          return Response.json({
            success: false,
            error: "Invalid scheduledAt date"
          }, { status: 400 });
        }
        if (scheduledDate <= /* @__PURE__ */ new Date()) {
          return Response.json({
            success: false,
            error: "scheduledAt must be in the future"
          }, { status: 400 });
        }
        const broadcastDoc = await req.payload.findByID({
          collection: collectionSlug,
          id,
          user: auth.user
        });
        if (!broadcastDoc || !broadcastDoc.providerId) {
          return Response.json({
            success: false,
            error: "Broadcast not found or not synced with provider"
          }, { status: 404 });
        }
        const providerConfig = config.providers?.broadcast;
        if (!providerConfig) {
          return Response.json({
            success: false,
            error: "Broadcast provider not configured"
          }, { status: 500 });
        }
        const { BroadcastApiProvider: BroadcastApiProvider2 } = await Promise.resolve().then(() => (init_broadcast2(), broadcast_exports));
        const provider = new BroadcastApiProvider2(providerConfig);
        const broadcast = await provider.schedule(broadcastDoc.providerId, scheduledDate);
        await req.payload.update({
          collection: collectionSlug,
          id,
          data: {
            sendStatus: "scheduled" /* SCHEDULED */,
            scheduledAt: scheduledDate.toISOString()
          },
          user: auth.user
        });
        return Response.json({
          success: true,
          message: `Broadcast scheduled for ${scheduledDate.toISOString()}`,
          broadcast
        });
      } catch (error) {
        console.error("Failed to schedule broadcast:", error);
        if (error instanceof NewsletterProviderError) {
          return Response.json({
            success: false,
            error: error.message,
            code: error.code
          }, { status: error.code === "NOT_SUPPORTED" ? 501 : 500 });
        }
        return Response.json({
          success: false,
          error: "Failed to schedule broadcast"
        }, { status: 500 });
      }
    }
  };
};

// src/endpoints/broadcasts/test.ts
var createTestBroadcastEndpoint = (config, collectionSlug) => {
  return {
    path: "/:id/test",
    method: "post",
    handler: async (req) => {
      try {
        const auth = await requireAdmin(req, config);
        if (!auth.authorized) {
          return Response.json({
            success: false,
            error: auth.error
          }, { status: 401 });
        }
        const url = new URL(req.url || "", `http://localhost`);
        const pathParts = url.pathname.split("/");
        const id = pathParts[pathParts.length - 2];
        if (!id) {
          return Response.json({
            success: false,
            error: "Broadcast ID is required"
          }, { status: 400 });
        }
        const data = await (req.json?.() || Promise.resolve({}));
        const testEmail = data.email || auth.user.email;
        if (!testEmail) {
          return Response.json({
            success: false,
            error: "No email address available for test send"
          }, { status: 400 });
        }
        const broadcast = await req.payload.findByID({
          collection: collectionSlug,
          id,
          user: auth.user
        });
        if (!broadcast) {
          return Response.json({
            success: false,
            error: "Broadcast not found"
          }, { status: 404 });
        }
        const htmlContent = await convertToEmailSafeHtml(broadcast.content, {
          wrapInTemplate: true,
          preheader: broadcast.preheader,
          customBlockConverter: config.customizations?.broadcasts?.customBlockConverter
        });
        const emailService = req.payload.newsletterEmailService;
        if (!emailService) {
          return Response.json({
            success: false,
            error: "Email service is not configured"
          }, { status: 500 });
        }
        const providerConfig = config.providers.default === "resend" ? config.providers.resend : config.providers.broadcast;
        const fromEmail = providerConfig?.fromAddress || providerConfig?.fromEmail || "noreply@example.com";
        const fromName = providerConfig?.fromName || "Newsletter";
        const replyTo = broadcast.settings?.replyTo || providerConfig?.replyTo;
        await emailService.send({
          to: testEmail,
          from: fromEmail,
          fromName,
          replyTo,
          subject: `[TEST] ${broadcast.subject}`,
          html: htmlContent,
          trackOpens: false,
          trackClicks: false
        });
        return Response.json({
          success: true,
          message: `Test email sent to ${testEmail}`
        });
      } catch (error) {
        console.error("Failed to send test broadcast:", error);
        return Response.json({
          success: false,
          error: "Failed to send test email"
        }, { status: 500 });
      }
    }
  };
};

// src/endpoints/broadcasts/preview.ts
async function populateMediaFields(content, payload, config) {
  if (!content || typeof content !== "object") return content;
  if (content.root?.children) {
    for (const child of content.root.children) {
      await populateBlockMediaFields(child, payload, config);
    }
  }
  return content;
}
async function populateBlockMediaFields(node, payload, config) {
  if (node.type === "block" && node.fields) {
    const blockType = node.fields.blockType || node.fields.blockName;
    const customBlocks = config.customizations?.broadcasts?.customBlocks || [];
    const blockConfig = customBlocks.find((b) => b.slug === blockType);
    if (blockConfig && blockConfig.fields) {
      for (const field of blockConfig.fields) {
        if (field.type === "upload" && field.relationTo && node.fields[field.name]) {
          const fieldValue = node.fields[field.name];
          if (typeof fieldValue === "string" && fieldValue.match(/^[a-f0-9]{24}$/i)) {
            try {
              const media = await payload.findByID({
                collection: field.relationTo,
                id: fieldValue,
                depth: 0
              });
              if (media) {
                node.fields[field.name] = media;
                payload.logger?.info(`Populated ${field.name} for block ${blockType}:`, {
                  mediaId: fieldValue,
                  mediaUrl: media.url,
                  filename: media.filename
                });
              }
            } catch (error) {
              payload.logger?.error(`Failed to populate ${field.name} for block ${blockType}:`, error);
            }
          }
        }
        if (field.type === "array" && field.fields) {
          const arrayValue = node.fields[field.name];
          if (Array.isArray(arrayValue)) {
            for (const arrayItem of arrayValue) {
              if (arrayItem && typeof arrayItem === "object") {
                for (const arrayField of field.fields) {
                  if (arrayField.type === "upload" && arrayField.relationTo && arrayItem[arrayField.name]) {
                    const arrayFieldValue = arrayItem[arrayField.name];
                    if (typeof arrayFieldValue === "string" && arrayFieldValue.match(/^[a-f0-9]{24}$/i)) {
                      try {
                        const media = await payload.findByID({
                          collection: arrayField.relationTo,
                          id: arrayFieldValue,
                          depth: 0
                        });
                        if (media) {
                          arrayItem[arrayField.name] = media;
                          payload.logger?.info(`Populated array ${arrayField.name} for block ${blockType}:`, {
                            mediaId: arrayFieldValue,
                            mediaUrl: media.url,
                            filename: media.filename
                          });
                        }
                      } catch (error) {
                        payload.logger?.error(`Failed to populate array ${arrayField.name} for block ${blockType}:`, error);
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
  if (node.children) {
    for (const child of node.children) {
      await populateBlockMediaFields(child, payload, config);
    }
  }
}
var createBroadcastPreviewEndpoint = (config, _collectionSlug) => {
  return {
    path: "/preview",
    method: "post",
    handler: async (req) => {
      try {
        const data = await (req.json?.() || Promise.resolve({}));
        const { content, preheader, subject, documentData } = data;
        if (!content) {
          return Response.json({
            success: false,
            error: "Content is required for preview"
          }, { status: 400 });
        }
        const mediaUrl = req.payload.config.serverURL ? `${req.payload.config.serverURL}/api/media` : "/api/media";
        req.payload.logger?.info("Populating media fields for email preview...");
        const populatedContent = await populateMediaFields(content, req.payload, config);
        const emailPreviewConfig = config.customizations?.broadcasts?.emailPreview;
        const htmlContent = await convertToEmailSafeHtml(populatedContent, {
          wrapInTemplate: emailPreviewConfig?.wrapInTemplate ?? true,
          preheader,
          subject,
          mediaUrl,
          documentData,
          // Pass all document data
          customBlockConverter: config.customizations?.broadcasts?.customBlockConverter,
          customWrapper: emailPreviewConfig?.customWrapper
        });
        return Response.json({
          success: true,
          html: htmlContent,
          preview: {
            subject: subject || "Preview",
            preheader: preheader || "",
            html: htmlContent
          }
        });
      } catch (error) {
        console.error("Failed to generate email preview:", error);
        return Response.json({
          success: false,
          error: "Failed to generate email preview"
        }, { status: 500 });
      }
    }
  };
};

// src/collections/Broadcasts.ts
var createBroadcastsCollection = (pluginConfig) => {
  const hasProviders = !!(pluginConfig.providers?.broadcast || pluginConfig.providers?.resend);
  const customizations = pluginConfig.customizations?.broadcasts;
  const collectionSlug = "broadcasts";
  return {
    slug: collectionSlug,
    access: {
      read: () => true,
      // Public read access
      create: ({ req: { user } }) => {
        return Boolean(user);
      },
      update: ({ req: { user } }) => {
        return Boolean(user);
      },
      delete: ({ req: { user } }) => {
        return Boolean(user);
      }
    },
    versions: {
      drafts: {
        autosave: true,
        schedulePublish: true
      }
    },
    labels: {
      singular: "Broadcast",
      plural: "Broadcasts"
    },
    admin: {
      useAsTitle: "subject",
      description: "Individual email campaigns sent to subscribers",
      defaultColumns: ["subject", "_status", "sendStatus", "sentAt", "recipientCount"]
    },
    endpoints: [
      createSendBroadcastEndpoint(pluginConfig, collectionSlug),
      createScheduleBroadcastEndpoint(pluginConfig, collectionSlug),
      createTestBroadcastEndpoint(pluginConfig, collectionSlug),
      createBroadcastPreviewEndpoint(pluginConfig, collectionSlug)
    ],
    fields: [
      {
        name: "subject",
        type: "text",
        required: true,
        admin: {
          description: "Email subject line"
        }
      },
      // Add any additional fields from customizations after subject
      ...customizations?.additionalFields || [],
      {
        type: "row",
        fields: [
          {
            name: "contentSection",
            type: "group",
            label: false,
            admin: {
              width: "50%",
              style: {
                paddingRight: "1rem"
              }
            },
            fields: [
              {
                name: "preheader",
                type: "text",
                admin: {
                  description: "Preview text shown in email clients"
                }
              },
              // Apply content field customization if provided
              // Process blocks server-side to avoid client serialization issues
              (() => {
                const emailEditor = createEmailLexicalEditor(customizations?.customBlocks);
                const baseField = createEmailContentField({
                  admin: { description: "Email content" },
                  editor: emailEditor
                });
                return customizations?.fieldOverrides?.content ? customizations.fieldOverrides.content(baseField) : baseField;
              })()
            ]
          },
          {
            name: "previewSection",
            type: "group",
            label: false,
            admin: {
              width: "50%"
            },
            fields: [
              createBroadcastInlinePreviewField()
            ]
          }
        ]
      },
      {
        name: "sendStatus",
        type: "select",
        label: "Send Status",
        required: true,
        defaultValue: "draft" /* DRAFT */,
        options: [
          { label: "Draft", value: "draft" /* DRAFT */ },
          { label: "Scheduled", value: "scheduled" /* SCHEDULED */ },
          { label: "Sending", value: "sending" /* SENDING */ },
          { label: "Sent", value: "sent" /* SENT */ },
          { label: "Failed", value: "failed" /* FAILED */ },
          { label: "Paused", value: "paused" /* PAUSED */ },
          { label: "Canceled", value: "canceled" /* CANCELED */ }
        ],
        admin: {
          readOnly: true,
          description: "The status of the email send operation",
          components: {
            Cell: "payload-plugin-newsletter/components#StatusBadge"
          }
        }
      },
      {
        name: "settings",
        type: "group",
        fields: [
          {
            name: "trackOpens",
            type: "checkbox",
            defaultValue: true,
            admin: {
              description: "Track when recipients open this email"
            }
          },
          {
            name: "trackClicks",
            type: "checkbox",
            defaultValue: true,
            admin: {
              description: "Track when recipients click links"
            }
          },
          {
            name: "replyTo",
            type: "email",
            admin: {
              description: "Override the channel reply-to address for this broadcast"
            }
          }
        ]
      },
      {
        name: "audienceIds",
        type: "array",
        fields: [
          {
            name: "audienceId",
            type: "text",
            required: true
          }
        ],
        admin: {
          description: "Target specific audience segments",
          condition: () => {
            return hasProviders;
          }
        }
      },
      {
        name: "analytics",
        type: "group",
        admin: {
          readOnly: true,
          condition: (data) => data?.sendStatus === "sent" /* SENT */
        },
        fields: [
          {
            name: "recipientCount",
            type: "number",
            defaultValue: 0
          },
          {
            name: "sent",
            type: "number",
            defaultValue: 0
          },
          {
            name: "delivered",
            type: "number",
            defaultValue: 0
          },
          {
            name: "opened",
            type: "number",
            defaultValue: 0
          },
          {
            name: "clicked",
            type: "number",
            defaultValue: 0
          },
          {
            name: "bounced",
            type: "number",
            defaultValue: 0
          },
          {
            name: "complained",
            type: "number",
            defaultValue: 0
          },
          {
            name: "unsubscribed",
            type: "number",
            defaultValue: 0
          }
        ]
      },
      {
        name: "sentAt",
        type: "date",
        admin: {
          readOnly: true,
          date: {
            displayFormat: "MMM d, yyyy h:mm a"
          }
        }
      },
      {
        name: "scheduledAt",
        type: "date",
        admin: {
          condition: (data) => data?.sendStatus === "scheduled" /* SCHEDULED */,
          date: {
            displayFormat: "MMM d, yyyy h:mm a"
          }
        }
      },
      {
        name: "providerId",
        type: "text",
        admin: {
          readOnly: true,
          description: "ID from the email provider",
          condition: (data) => hasProviders && data?.providerId
        }
      },
      {
        name: "providerData",
        type: "json",
        admin: {
          readOnly: true,
          condition: () => false
          // Hidden by default
        }
      }
    ],
    hooks: {
      // Sync with provider on create and update
      afterChange: [
        async ({ doc, operation, req, previousDoc }) => {
          if (!hasProviders) return doc;
          if (operation === "create") {
            if (!doc.subject || !doc.contentSection?.content) {
              req.payload.logger.info("Skipping provider sync - broadcast has no subject or content yet");
              return doc;
            }
            try {
              const providerConfig = await getBroadcastConfig(req, pluginConfig);
              if (!providerConfig || !providerConfig.token) {
                req.payload.logger.error("Broadcast provider not configured in Newsletter Settings or environment variables");
                return doc;
              }
              const { BroadcastApiProvider: BroadcastApiProvider2 } = await Promise.resolve().then(() => (init_broadcast2(), broadcast_exports));
              const provider = new BroadcastApiProvider2(providerConfig);
              req.payload.logger.info("Populating media fields and converting content to HTML...");
              const populatedContent = await populateMediaFields(doc.contentSection?.content, req.payload, pluginConfig);
              const emailPreviewConfig = pluginConfig.customizations?.broadcasts?.emailPreview;
              const htmlContent = await convertToEmailSafeHtml(populatedContent, {
                wrapInTemplate: emailPreviewConfig?.wrapInTemplate ?? true,
                customWrapper: emailPreviewConfig?.customWrapper,
                preheader: doc.contentSection?.preheader,
                subject: doc.subject,
                documentData: doc,
                // Pass entire document
                customBlockConverter: pluginConfig.customizations?.broadcasts?.customBlockConverter
              });
              if (!htmlContent || htmlContent.trim() === "") {
                req.payload.logger.info("Skipping provider sync - content is empty after conversion");
                return doc;
              }
              const createData = {
                name: doc.subject,
                // Use subject as name since we removed the name field
                subject: doc.subject,
                preheader: doc.contentSection?.preheader,
                content: htmlContent,
                trackOpens: doc.settings?.trackOpens,
                trackClicks: doc.settings?.trackClicks,
                replyTo: doc.settings?.replyTo || providerConfig.replyTo,
                audienceIds: doc.audienceIds?.map((a) => a.audienceId)
              };
              req.payload.logger.info("Creating broadcast with data:", {
                name: createData.name,
                subject: createData.subject,
                preheader: createData.preheader || "NONE",
                contentLength: htmlContent ? htmlContent.length : 0,
                contentPreview: htmlContent ? htmlContent.substring(0, 100) + "..." : "EMPTY",
                trackOpens: createData.trackOpens,
                trackClicks: createData.trackClicks,
                replyTo: createData.replyTo,
                audienceIds: createData.audienceIds || [],
                apiUrl: providerConfig.apiUrl,
                hasToken: !!providerConfig.token
              });
              const providerBroadcast = await provider.create(createData);
              await req.payload.update({
                collection: "broadcasts",
                id: doc.id,
                data: {
                  providerId: providerBroadcast.id,
                  providerData: providerBroadcast.providerData
                },
                req
              });
              return {
                ...doc,
                providerId: providerBroadcast.id,
                providerData: providerBroadcast.providerData
              };
            } catch (error) {
              req.payload.logger.error("Raw error from broadcast provider:");
              req.payload.logger.error(error);
              if (error instanceof Error) {
                req.payload.logger.error("Error is instance of Error:", {
                  message: error.message,
                  stack: error.stack,
                  name: error.name,
                  // If it's a BroadcastProviderError, it might have additional details
                  ...error.details,
                  // Check if it's a fetch response error
                  ...error.response,
                  ...error.data,
                  ...error.status,
                  ...error.statusText
                });
              } else if (typeof error === "string") {
                req.payload.logger.error("Error is a string:", error);
              } else if (error && typeof error === "object") {
                req.payload.logger.error("Error is an object:", JSON.stringify(error, null, 2));
              } else {
                req.payload.logger.error("Unknown error type:", typeof error);
              }
              req.payload.logger.error("Failed broadcast document:", {
                id: doc.id,
                subject: doc.subject,
                hasContent: !!doc.contentSection?.content,
                contentType: doc.contentSection?.content ? typeof doc.contentSection.content : "none"
              });
              return doc;
            }
          }
          if (operation === "update") {
            req.payload.logger.info("Broadcast afterChange update hook triggered", {
              operation,
              hasProviderId: !!doc.providerId,
              sendStatus: doc.sendStatus,
              publishStatus: doc._status
            });
            try {
              const providerConfig = await getBroadcastConfig(req, pluginConfig);
              if (!providerConfig || !providerConfig.token) {
                req.payload.logger.error("Broadcast provider not configured in Newsletter Settings or environment variables");
                return doc;
              }
              const { BroadcastApiProvider: BroadcastApiProvider2 } = await Promise.resolve().then(() => (init_broadcast2(), broadcast_exports));
              const provider = new BroadcastApiProvider2(providerConfig);
              if (!doc.providerId) {
                if (!doc.subject || !doc.contentSection?.content) {
                  req.payload.logger.info("Still missing required fields for provider sync");
                  return doc;
                }
                req.payload.logger.info("Creating broadcast in provider (deferred from initial create)...");
                const populatedContent = await populateMediaFields(doc.contentSection?.content, req.payload, pluginConfig);
                const emailPreviewConfig = pluginConfig.customizations?.broadcasts?.emailPreview;
                const htmlContent = await convertToEmailSafeHtml(populatedContent, {
                  wrapInTemplate: emailPreviewConfig?.wrapInTemplate ?? true,
                  customWrapper: emailPreviewConfig?.customWrapper,
                  preheader: doc.contentSection?.preheader,
                  subject: doc.subject,
                  documentData: doc,
                  // Pass entire document
                  customBlockConverter: pluginConfig.customizations?.broadcasts?.customBlockConverter
                });
                if (!htmlContent || htmlContent.trim() === "") {
                  req.payload.logger.info("Skipping provider sync - content is empty after conversion");
                  return doc;
                }
                const createData = {
                  name: doc.subject,
                  subject: doc.subject,
                  preheader: doc.contentSection?.preheader,
                  content: htmlContent,
                  trackOpens: doc.settings?.trackOpens,
                  trackClicks: doc.settings?.trackClicks,
                  replyTo: doc.settings?.replyTo || providerConfig.replyTo,
                  audienceIds: doc.audienceIds?.map((a) => a.audienceId)
                };
                req.payload.logger.info("Creating broadcast with data:", {
                  name: createData.name,
                  subject: createData.subject,
                  preheader: createData.preheader || "NONE",
                  contentLength: htmlContent ? htmlContent.length : 0,
                  contentPreview: htmlContent ? htmlContent.substring(0, 100) + "..." : "EMPTY",
                  apiUrl: providerConfig.apiUrl,
                  hasToken: !!providerConfig.token
                });
                const providerBroadcast = await provider.create(createData);
                await req.payload.update({
                  collection: "broadcasts",
                  id: doc.id,
                  data: {
                    providerId: providerBroadcast.id,
                    providerData: providerBroadcast.providerData
                  },
                  req
                });
                req.payload.logger.info(`Broadcast ${doc.id} created in provider successfully (deferred)`);
                return {
                  ...doc,
                  providerId: providerBroadcast.id,
                  providerData: providerBroadcast.providerData
                };
              }
              if (doc.providerId) {
                const capabilities = provider.getCapabilities();
                const sendStatus = doc.sendStatus || "draft" /* DRAFT */;
                if (!capabilities.editableStatuses.includes(sendStatus)) {
                  req.payload.logger.info(`Skipping sync for broadcast in status: ${sendStatus}`);
                  return doc;
                }
                const contentChanged = doc.subject !== previousDoc?.subject || doc.contentSection?.preheader !== previousDoc?.contentSection?.preheader || JSON.stringify(doc.contentSection?.content) !== JSON.stringify(previousDoc?.contentSection?.content) || doc.settings?.trackOpens !== previousDoc?.settings?.trackOpens || doc.settings?.trackClicks !== previousDoc?.settings?.trackClicks || doc.settings?.replyTo !== previousDoc?.settings?.replyTo || JSON.stringify(doc.audienceIds) !== JSON.stringify(previousDoc?.audienceIds);
                if (contentChanged) {
                  const updates = {};
                  if (doc.subject !== previousDoc?.subject) {
                    updates.name = doc.subject;
                    updates.subject = doc.subject;
                  }
                  if (doc.contentSection?.preheader !== previousDoc?.contentSection?.preheader) {
                    updates.preheader = doc.contentSection?.preheader;
                  }
                  if (JSON.stringify(doc.contentSection?.content) !== JSON.stringify(previousDoc?.contentSection?.content)) {
                    const populatedContent = await populateMediaFields(doc.contentSection?.content, req.payload, pluginConfig);
                    const emailPreviewConfig = pluginConfig.customizations?.broadcasts?.emailPreview;
                    updates.content = await convertToEmailSafeHtml(populatedContent, {
                      wrapInTemplate: emailPreviewConfig?.wrapInTemplate ?? true,
                      customWrapper: emailPreviewConfig?.customWrapper,
                      preheader: doc.contentSection?.preheader,
                      subject: doc.subject,
                      documentData: doc,
                      // Pass entire document
                      customBlockConverter: pluginConfig.customizations?.broadcasts?.customBlockConverter
                    });
                  }
                  if (doc.settings?.trackOpens !== previousDoc?.settings?.trackOpens) {
                    updates.trackOpens = doc.settings.trackOpens;
                  }
                  if (doc.settings?.trackClicks !== previousDoc?.settings?.trackClicks) {
                    updates.trackClicks = doc.settings.trackClicks;
                  }
                  if (doc.settings?.replyTo !== previousDoc?.settings?.replyTo) {
                    updates.replyTo = doc.settings.replyTo || providerConfig.replyTo;
                  }
                  if (JSON.stringify(doc.audienceIds) !== JSON.stringify(previousDoc?.audienceIds)) {
                    updates.audienceIds = doc.audienceIds?.map((a) => a.audienceId);
                  }
                  req.payload.logger.info("Syncing broadcast updates to provider", {
                    providerId: doc.providerId,
                    updates
                  });
                  await provider.update(doc.providerId, updates);
                  req.payload.logger.info(`Broadcast ${doc.id} synced to provider successfully`);
                } else {
                  req.payload.logger.info("No content changes to sync to provider");
                }
              }
            } catch (error) {
              req.payload.logger.error("Raw error from broadcast update operation:");
              req.payload.logger.error(error);
              if (error instanceof Error) {
                req.payload.logger.error("Error is instance of Error:", {
                  message: error.message,
                  stack: error.stack,
                  name: error.name,
                  ...error.details,
                  ...error.response,
                  ...error.data,
                  ...error.status,
                  ...error.statusText
                });
              } else if (typeof error === "string") {
                req.payload.logger.error("Error is a string:", error);
              } else if (error && typeof error === "object") {
                req.payload.logger.error("Error is an object:", JSON.stringify(error, null, 2));
              } else {
                req.payload.logger.error("Unknown error type:", typeof error);
              }
              req.payload.logger.error("Failed broadcast document (update operation):", {
                id: doc.id,
                subject: doc.subject,
                hasContent: !!doc.contentSection?.content,
                contentType: doc.contentSection?.content ? typeof doc.contentSection.content : "none"
              });
            }
          }
          return doc;
        },
        // Hook to send when published
        async ({ doc, operation, previousDoc, req }) => {
          if (operation !== "update") return doc;
          const wasUnpublished = !previousDoc?._status || previousDoc._status === "draft";
          const isNowPublished = doc._status === "published";
          if (wasUnpublished && isNowPublished && doc.providerId) {
            if (doc.sendStatus === "sent" /* SENT */ || doc.sendStatus === "sending" /* SENDING */) {
              return doc;
            }
            try {
              const broadcastConfig = await getBroadcastConfig(req, pluginConfig);
              const resendConfig = pluginConfig.providers?.resend;
              if (!broadcastConfig && !resendConfig) {
                req.payload.logger.error("No provider configured for sending in Newsletter Settings or environment variables");
                return doc;
              }
              if (broadcastConfig && broadcastConfig.token) {
                const { BroadcastApiProvider: BroadcastApiProvider2 } = await Promise.resolve().then(() => (init_broadcast2(), broadcast_exports));
                const provider = new BroadcastApiProvider2(broadcastConfig);
                await provider.send(doc.providerId);
              }
              await req.payload.update({
                collection: "broadcasts",
                id: doc.id,
                data: {
                  sendStatus: "sending" /* SENDING */,
                  sentAt: (/* @__PURE__ */ new Date()).toISOString()
                },
                req
              });
              req.payload.logger.info(`Broadcast ${doc.id} sent successfully`);
            } catch (error) {
              if (error instanceof Error) {
                req.payload.logger.error(`Failed to send broadcast ${doc.id}:`, {
                  message: error.message,
                  stack: error.stack,
                  name: error.name,
                  // If it's a BroadcastProviderError, it might have additional details
                  ...error.details
                });
              } else {
                req.payload.logger.error(`Failed to send broadcast ${doc.id}:`, error);
              }
              await req.payload.update({
                collection: "broadcasts",
                id: doc.id,
                data: {
                  sendStatus: "failed" /* FAILED */
                },
                req
              });
            }
          }
          return doc;
        }
      ],
      // beforeChange hooks can be added here if needed
      beforeChange: [],
      // Handle deletion
      afterDelete: [
        async ({ doc, req }) => {
          if (!hasProviders || !doc?.providerId) return doc;
          try {
            const providerConfig = await getBroadcastConfig(req, pluginConfig);
            if (!providerConfig || !providerConfig.token) {
              req.payload.logger.error("Broadcast provider not configured in Newsletter Settings or environment variables");
              return doc;
            }
            const { BroadcastApiProvider: BroadcastApiProvider2 } = await Promise.resolve().then(() => (init_broadcast2(), broadcast_exports));
            const provider = new BroadcastApiProvider2(providerConfig);
            const capabilities = provider.getCapabilities();
            if (capabilities.editableStatuses.includes(doc.sendStatus)) {
              await provider.delete(doc.providerId);
            }
          } catch (error) {
            if (error instanceof Error) {
              req.payload.logger.error("Failed to delete broadcast from provider:", {
                message: error.message,
                stack: error.stack,
                name: error.name,
                // If it's a BroadcastProviderError, it might have additional details
                ...error.details
              });
            } else {
              req.payload.logger.error("Failed to delete broadcast from provider:", error);
            }
          }
          return doc;
        }
      ]
    }
  };
};

// src/emails/render.tsx
import { render } from "@react-email/render";

// src/emails/MagicLink.tsx
import {
  Body,
  Button,
  Container,
  Head,
  Hr,
  Html,
  Preview,
  Text
} from "@react-email/components";

// src/emails/styles.ts
var styles = {
  main: {
    backgroundColor: "#f6f9fc",
    fontFamily: '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif'
  },
  container: {
    backgroundColor: "#ffffff",
    border: "1px solid #f0f0f0",
    borderRadius: "5px",
    margin: "0 auto",
    padding: "45px",
    marginBottom: "64px",
    maxWidth: "500px"
  },
  heading: {
    fontSize: "24px",
    letterSpacing: "-0.5px",
    lineHeight: "1.3",
    fontWeight: "600",
    color: "#484848",
    margin: "0 0 20px",
    padding: "0"
  },
  text: {
    fontSize: "16px",
    lineHeight: "26px",
    fontWeight: "400",
    color: "#484848",
    margin: "16px 0"
  },
  button: {
    backgroundColor: "#000000",
    borderRadius: "5px",
    color: "#fff",
    fontSize: "16px",
    fontWeight: "bold",
    textDecoration: "none",
    textAlign: "center",
    display: "block",
    width: "100%",
    padding: "14px 20px",
    margin: "30px 0"
  },
  link: {
    color: "#2754C5",
    fontSize: "14px",
    textDecoration: "underline",
    wordBreak: "break-all"
  },
  hr: {
    borderColor: "#e6ebf1",
    margin: "30px 0"
  },
  footer: {
    fontSize: "14px",
    lineHeight: "24px",
    color: "#9ca2ac",
    textAlign: "center",
    margin: "0"
  },
  code: {
    display: "inline-block",
    padding: "16px",
    width: "100%",
    backgroundColor: "#f4f4f4",
    borderRadius: "5px",
    border: "1px solid #eee",
    fontSize: "14px",
    fontFamily: "monospace",
    textAlign: "center",
    margin: "24px 0"
  }
};

// src/emails/MagicLink.tsx
import { jsx, jsxs } from "react/jsx-runtime";
var MagicLinkEmail = ({
  magicLink,
  email,
  siteName = "Newsletter",
  expiresIn = "24 hours"
}) => {
  const previewText = `Sign in to ${siteName}`;
  return /* @__PURE__ */ jsxs(Html, { children: [
    /* @__PURE__ */ jsx(Head, {}),
    /* @__PURE__ */ jsx(Preview, { children: previewText }),
    /* @__PURE__ */ jsx(Body, { style: styles.main, children: /* @__PURE__ */ jsxs(Container, { style: styles.container, children: [
      /* @__PURE__ */ jsxs(Text, { style: styles.heading, children: [
        "Sign in to ",
        siteName
      ] }),
      /* @__PURE__ */ jsxs(Text, { style: styles.text, children: [
        "Hi ",
        email.split("@")[0],
        ","
      ] }),
      /* @__PURE__ */ jsxs(Text, { style: styles.text, children: [
        "We received a request to sign in to your ",
        siteName,
        " account. Click the button below to complete your sign in:"
      ] }),
      /* @__PURE__ */ jsxs(Button, { href: magicLink, style: styles.button, children: [
        "Sign in to ",
        siteName
      ] }),
      /* @__PURE__ */ jsx(Text, { style: styles.text, children: "Or copy and paste this URL into your browser:" }),
      /* @__PURE__ */ jsx("code", { style: styles.code, children: magicLink }),
      /* @__PURE__ */ jsx(Hr, { style: styles.hr }),
      /* @__PURE__ */ jsxs(Text, { style: styles.footer, children: [
        "This link will expire in ",
        expiresIn,
        ". If you didn't request this email, you can safely ignore it."
      ] })
    ] }) })
  ] });
};

// src/emails/Welcome.tsx
import {
  Body as Body2,
  Button as Button2,
  Container as Container2,
  Head as Head2,
  Hr as Hr2,
  Html as Html2,
  Preview as Preview2,
  Text as Text2
} from "@react-email/components";
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
var WelcomeEmail = ({
  email,
  siteName = "Newsletter",
  preferencesUrl
}) => {
  const previewText = `Welcome to ${siteName}!`;
  const firstName = email.split("@")[0];
  return /* @__PURE__ */ jsxs2(Html2, { children: [
    /* @__PURE__ */ jsx2(Head2, {}),
    /* @__PURE__ */ jsx2(Preview2, { children: previewText }),
    /* @__PURE__ */ jsx2(Body2, { style: styles.main, children: /* @__PURE__ */ jsxs2(Container2, { style: styles.container, children: [
      /* @__PURE__ */ jsxs2(Text2, { style: styles.heading, children: [
        "Welcome to ",
        siteName,
        "! \u{1F389}"
      ] }),
      /* @__PURE__ */ jsxs2(Text2, { style: styles.text, children: [
        "Hi ",
        firstName,
        ","
      ] }),
      /* @__PURE__ */ jsxs2(Text2, { style: styles.text, children: [
        "Thanks for subscribing to ",
        siteName,
        "! We're excited to have you as part of our community."
      ] }),
      /* @__PURE__ */ jsx2(Text2, { style: styles.text, children: "You'll receive our newsletter based on your preferences. Speaking of which, you can update your preferences anytime:" }),
      preferencesUrl && /* @__PURE__ */ jsx2(Button2, { href: preferencesUrl, style: styles.button, children: "Manage Preferences" }),
      /* @__PURE__ */ jsx2(Text2, { style: styles.text, children: "Here's what you can expect from us:" }),
      /* @__PURE__ */ jsxs2(Text2, { style: styles.text, children: [
        "\u2022 Regular updates based on your chosen frequency",
        /* @__PURE__ */ jsx2("br", {}),
        "\u2022 Content tailored to your interests",
        /* @__PURE__ */ jsx2("br", {}),
        "\u2022 Easy unsubscribe options in every email",
        /* @__PURE__ */ jsx2("br", {}),
        "\u2022 Your privacy respected always"
      ] }),
      /* @__PURE__ */ jsx2(Hr2, { style: styles.hr }),
      /* @__PURE__ */ jsx2(Text2, { style: styles.footer, children: "If you have any questions, feel free to reply to this email. We're here to help!" })
    ] }) })
  ] });
};

// src/emails/SignIn.tsx
import { jsx as jsx3 } from "react/jsx-runtime";
var SignInEmail = (props) => {
  return /* @__PURE__ */ jsx3(MagicLinkEmail, { ...props });
};

// src/emails/render.tsx
import { jsx as jsx4 } from "react/jsx-runtime";
async function renderEmail(template, data, config) {
  try {
    if (config?.customTemplates) {
      const customTemplate = config.customTemplates[template];
      if (customTemplate) {
        const CustomComponent = customTemplate;
        return render(/* @__PURE__ */ jsx4(CustomComponent, { ...data }));
      }
    }
    switch (template) {
      case "magic-link": {
        const magicLinkData = data;
        return render(
          /* @__PURE__ */ jsx4(
            MagicLinkEmail,
            {
              magicLink: magicLinkData.magicLink || magicLinkData.verificationUrl || magicLinkData.magicLinkUrl || "",
              email: magicLinkData.email || "",
              siteName: magicLinkData.siteName,
              expiresIn: magicLinkData.expiresIn
            }
          )
        );
      }
      case "signin": {
        const signinData = data;
        return render(
          /* @__PURE__ */ jsx4(
            SignInEmail,
            {
              magicLink: signinData.magicLink || signinData.verificationUrl || signinData.magicLinkUrl || "",
              email: signinData.email || "",
              siteName: signinData.siteName,
              expiresIn: signinData.expiresIn
            }
          )
        );
      }
      case "welcome": {
        const welcomeData = data;
        return render(
          /* @__PURE__ */ jsx4(
            WelcomeEmail,
            {
              email: welcomeData.email || "",
              siteName: welcomeData.siteName,
              preferencesUrl: welcomeData.preferencesUrl
            }
          )
        );
      }
      default:
        throw new Error(`Unknown email template: ${template}`);
    }
  } catch (error) {
    console.error(`Failed to render email template ${template}:`, error);
    throw error;
  }
}

// src/collections/Subscribers.ts
var createSubscribersCollection = (pluginConfig) => {
  const slug = pluginConfig.subscribersSlug || "subscribers";
  const defaultFields = [
    // Core fields
    {
      name: "email",
      type: "email",
      required: true,
      unique: true,
      admin: {
        description: "Subscriber email address"
      }
    },
    {
      name: "name",
      type: "text",
      admin: {
        description: "Subscriber full name"
      }
    },
    {
      name: "locale",
      type: "select",
      options: pluginConfig.i18n?.locales?.map((locale) => ({
        label: locale.toUpperCase(),
        value: locale
      })) || [
        { label: "EN", value: "en" }
      ],
      defaultValue: pluginConfig.i18n?.defaultLocale || "en",
      admin: {
        description: "Preferred language for communications"
      }
    },
    // Authentication fields (hidden from admin UI)
    {
      name: "magicLinkToken",
      type: "text",
      hidden: true
    },
    {
      name: "magicLinkTokenExpiry",
      type: "date",
      hidden: true
    },
    // Subscription status
    {
      name: "subscriptionStatus",
      type: "select",
      options: [
        { label: "Active", value: "active" },
        { label: "Unsubscribed", value: "unsubscribed" },
        { label: "Pending", value: "pending" }
      ],
      defaultValue: "pending",
      required: true,
      admin: {
        description: "Current subscription status"
      }
    },
    {
      name: "unsubscribedAt",
      type: "date",
      admin: {
        condition: (data) => data?.subscriptionStatus === "unsubscribed",
        description: "When the user unsubscribed",
        readOnly: true
      }
    },
    // Email preferences
    {
      name: "emailPreferences",
      type: "group",
      fields: [
        {
          name: "newsletter",
          type: "checkbox",
          defaultValue: true,
          label: "Newsletter",
          admin: {
            description: "Receive regular newsletter updates"
          }
        },
        {
          name: "announcements",
          type: "checkbox",
          defaultValue: true,
          label: "Announcements",
          admin: {
            description: "Receive important announcements"
          }
        }
      ],
      admin: {
        description: "Email communication preferences"
      }
    },
    // Source tracking
    {
      name: "source",
      type: "text",
      admin: {
        description: "Where the subscriber signed up from"
      }
    }
  ];
  if (pluginConfig.features?.utmTracking?.enabled) {
    const utmFields = pluginConfig.features.utmTracking.fields || [
      "source",
      "medium",
      "campaign",
      "content",
      "term"
    ];
    defaultFields.push({
      name: "utmParameters",
      type: "group",
      fields: utmFields.map((field) => ({
        name: field,
        type: "text",
        admin: {
          description: `UTM ${field} parameter`
        }
      })),
      admin: {
        description: "UTM tracking parameters"
      }
    });
  }
  defaultFields.push({
    name: "signupMetadata",
    type: "group",
    fields: [
      {
        name: "ipAddress",
        type: "text",
        admin: {
          readOnly: true
        }
      },
      {
        name: "userAgent",
        type: "text",
        admin: {
          readOnly: true
        }
      },
      {
        name: "referrer",
        type: "text",
        admin: {
          readOnly: true
        }
      },
      {
        name: "signupPage",
        type: "text",
        admin: {
          readOnly: true
        }
      }
    ],
    admin: {
      description: "Technical information about signup"
    }
  });
  if (pluginConfig.features?.leadMagnets?.enabled) {
    defaultFields.push({
      name: "leadMagnet",
      type: "relationship",
      relationTo: pluginConfig.features.leadMagnets.collection || "media",
      admin: {
        description: "Lead magnet downloaded at signup"
      }
    });
  }
  let fields = defaultFields;
  if (pluginConfig.fields?.overrides) {
    fields = pluginConfig.fields.overrides({ defaultFields });
  }
  if (pluginConfig.fields?.additional) {
    fields = [...fields, ...pluginConfig.fields.additional];
  }
  const subscribersCollection = {
    slug,
    labels: {
      singular: "Subscriber",
      plural: "Subscribers"
    },
    admin: {
      useAsTitle: "email",
      defaultColumns: ["email", "name", "subscriptionStatus", "createdAt"],
      group: "Newsletter"
    },
    fields,
    hooks: {
      afterChange: [
        async ({ doc, req, operation, previousDoc }) => {
          if (operation === "create") {
            const emailService = req.payload.newsletterEmailService;
            console.log("[Newsletter Plugin] Creating subscriber:", {
              email: doc.email,
              hasEmailService: !!emailService
            });
            if (emailService) {
              try {
                await emailService.addContact(doc);
                console.log("[Newsletter Plugin] Successfully added contact to email service");
              } catch (error) {
                console.error("[Newsletter Plugin] Failed to add contact to email service:", error);
              }
            } else {
              console.warn("[Newsletter Plugin] No email service configured for subscriber creation");
            }
            if (doc.subscriptionStatus === "active" && emailService) {
              try {
                const settings = await req.payload.findGlobal({
                  slug: pluginConfig.settingsSlug || "newsletter-settings"
                });
                const serverURL = req.payload.config.serverURL || process.env.PAYLOAD_PUBLIC_SERVER_URL || "";
                const html = await renderEmail("welcome", {
                  email: doc.email,
                  siteName: settings?.brandSettings?.siteName || "Newsletter",
                  preferencesUrl: `${serverURL}/account/preferences`
                  // This could be customized
                }, pluginConfig);
                await emailService.send({
                  to: doc.email,
                  subject: settings?.brandSettings?.siteName ? `Welcome to ${settings.brandSettings.siteName}!` : "Welcome!",
                  html
                });
                console.warn(`Welcome email sent to: ${doc.email}`);
              } catch (error) {
                console.error("Failed to send welcome email:", error);
              }
            }
            if (pluginConfig.hooks?.afterSubscribe) {
              await pluginConfig.hooks.afterSubscribe({ doc, req });
            }
          }
          if (operation === "update" && previousDoc) {
            const emailService = req.payload.newsletterEmailService;
            if (doc.subscriptionStatus !== previousDoc.subscriptionStatus) {
              console.log("[Newsletter Plugin] Subscription status changed:", {
                email: doc.email,
                from: previousDoc.subscriptionStatus,
                to: doc.subscriptionStatus,
                hasEmailService: !!emailService
              });
              if (emailService) {
                try {
                  await emailService.updateContact(doc);
                  console.log("[Newsletter Plugin] Successfully updated contact in email service");
                } catch (error) {
                  console.error("[Newsletter Plugin] Failed to update contact in email service:", error);
                }
              } else {
                console.warn("[Newsletter Plugin] No email service configured");
              }
            }
            if (doc.subscriptionStatus === "unsubscribed" && previousDoc.subscriptionStatus !== "unsubscribed") {
              doc.unsubscribedAt = (/* @__PURE__ */ new Date()).toISOString();
              if (pluginConfig.hooks?.afterUnsubscribe) {
                await pluginConfig.hooks.afterUnsubscribe({ doc, req });
              }
            }
          }
        }
      ],
      beforeDelete: [
        async ({ id, req }) => {
          const emailService = req.payload.newsletterEmailService;
          if (emailService) {
            try {
              const doc = await req.payload.findByID({
                collection: slug,
                id
              });
              await emailService.removeContact(doc.email);
            } catch {
            }
          }
        }
      ]
    },
    access: {
      create: () => true,
      // Public can subscribe
      read: adminOrSelf(pluginConfig),
      update: adminOrSelf(pluginConfig),
      delete: adminOnly(pluginConfig)
    },
    timestamps: true
  };
  return subscribersCollection;
};
export {
  createBroadcastsCollection,
  createSubscribersCollection
};
//# sourceMappingURL=collections.js.map