UNPKG

payload-plugin-newsletter

Version:

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

3,871 lines 132 kB
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
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 });
};
var __copyProps = (to, from, except, desc) => {
  if (from && typeof from === "object" || typeof from === "function") {
    for (let key of __getOwnPropNames(from))
      if (!__hasOwnProp.call(to, key) && key !== except)
        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  }
  return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
  // If the importer is in node compatibility mode or this is not an ESM
  // file that has been converted to a CommonJS file using a Babel-
  // compatible transform (i.e. "__esModule" has not been set), then set
  // "default" to the CommonJS "module.exports" for node compatibility.
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
  mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);

// 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 {
          console.log("[BroadcastApiProvider] Getting broadcast with ID:", id);
          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();
          console.log("[BroadcastApiProvider] GET response:", broadcast);
          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 responseText = await response.text();
          console.log("[BroadcastApiProvider] Success response body:", responseText);
          let result;
          try {
            result = JSON.parse(responseText);
          } catch {
            throw new Error(`Failed to parse response as JSON: ${responseText}`);
          }
          console.log("[BroadcastApiProvider] Parsed result:", result);
          if (!result.id) {
            throw new Error(`Response missing expected 'id' field: ${JSON.stringify(result)}`);
          }
          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/exports/collections.ts
var collections_exports = {};
__export(collections_exports, {
  createBroadcastsCollection: () => createBroadcastsCollection,
  createSubscribersCollection: () => createSubscribersCollection
});
module.exports = __toCommonJS(collections_exports);

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

// src/fields/emailContent.ts
var import_richtext_lexical = require("@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
    (0, import_richtext_lexical.FixedToolbarFeature)(),
    // Fixed toolbar at the top
    (0, import_richtext_lexical.InlineToolbarFeature)(),
    // Floating toolbar when text is selected
    // Basic text formatting
    (0, import_richtext_lexical.BoldFeature)(),
    (0, import_richtext_lexical.ItalicFeature)(),
    (0, import_richtext_lexical.UnderlineFeature)(),
    (0, import_richtext_lexical.StrikethroughFeature)(),
    // Links - use default fields to ensure drawer UI works correctly
    (0, import_richtext_lexical.LinkFeature)(),
    // Lists
    (0, import_richtext_lexical.OrderedListFeature)(),
    (0, import_richtext_lexical.UnorderedListFeature)(),
    // Headings - limited to h1, h2, h3 for email compatibility
    (0, import_richtext_lexical.HeadingFeature)({
      enabledHeadingSizes: ["h1", "h2", "h3"]
    }),
    // Basic paragraph and alignment
    (0, import_richtext_lexical.ParagraphFeature)(),
    (0, import_richtext_lexical.AlignFeature)(),
    // Blockquotes
    (0, import_richtext_lexical.BlockquoteFeature)(),
    // Upload feature for images
    (0, import_richtext_lexical.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
    (0, import_richtext_lexical.BlocksFeature)({
      blocks: allBlocks
    })
  ];
};
var createEmailLexicalEditor = (customBlocks = []) => {
  const emailSafeBlocks = createEmailSafeBlocks(customBlocks);
  return (0, import_richtext_lexical.lexicalEditor)({
    features: [
      // Toolbars
      (0, import_richtext_lexical.FixedToolbarFeature)(),
      (0, import_richtext_lexical.InlineToolbarFeature)(),
      // Basic text formatting
      (0, import_richtext_lexical.BoldFeature)(),
      (0, import_richtext_lexical.ItalicFeature)(),
      (0, import_richtext_lexical.UnderlineFeature)(),
      (0, import_richtext_lexical.StrikethroughFeature)(),
      // Links - use default fields to ensure drawer UI works correctly
      (0, import_richtext_lexical.LinkFeature)(),
      // Lists
      (0, import_richtext_lexical.OrderedListFeature)(),
      (0, import_richtext_lexical.UnorderedListFeature)(),
      // Headings - limited to h1, h2, h3 for email compatibility
      (0, import_richtext_lexical.HeadingFeature)({
        enabledHeadingSizes: ["h1", "h2", "h3"]
      }),
      // Basic paragraph and alignment
      (0, import_richtext_lexical.ParagraphFeature)(),
      (0, import_richtext_lexical.AlignFeature)(),
      // Blockquotes
      (0, import_richtext_lexical.BlockquoteFeature)(),
      // Upload feature for images
      (0, import_richtext_lexical.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)
      (0, import_richtext_lexical.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/fields/broadcastSchedule.ts
var createBroadcastScheduleField = () => {
  return {
    name: "scheduleControls",
    type: "ui",
    label: "Scheduling",
    admin: {
      components: {
        Field: "payload-plugin-newsletter/components#BroadcastScheduleField"
      }
    }
  };
};

// src/utils/emailSafeHtml.ts
var import_isomorphic_dompurify = __toESM(require("isomorphic-dompurify"), 1);
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 = import_isomorphic_dompurify.default.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
var BROADCAST_CONFIG_CACHE_KEY = "broadcastConfig";
async function getBroadcastConfig(req, pluginConfig) {
  const cached = req.context?.[BROADCAST_CONFIG_CACHE_KEY];
  if (cached !== void 0) {
    return cached;
  }
  try {
    const settings = await req.payload.findGlobal({
      slug: pluginConfig.settingsSlug || "newsletter-settings",
      req
    });
    let result = null;
    if (settings?.provider === "broadcast" && settings?.broadcastSettings) {
      result = {
        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
      };
    } else {
      result = pluginConfig.providers?.broadcast || null;
    }
    if (req.context) {
      req.context[BROADCAST_CONFIG_CACHE_KEY] = result;
    }
    return result;
  } catch (error) {
    req.payload.logger.error({ error: String(error) }, "Failed to get broadcast config from settings");
    const fallback = pluginConfig.providers?.broadcast || null;
    if (req.context) {
      req.context[BROADCAST_CONFIG_CACHE_KEY] = fallback;
    }
    return fallback;
  }
}

// src/utils/getProvider.ts
init_broadcast();
async function getBroadcastProvider(req, pluginConfig) {
  const config = await getBroadcastConfig(req, pluginConfig);
  if (!config?.token) {
    throw new BroadcastProviderError(
      "Broadcast provider not configured. Check Newsletter Settings or environment variables.",
      "CONFIGURATION_ERROR" /* CONFIGURATION_ERROR */,
      "broadcast"
    );
  }
  const { BroadcastApiProvider: BroadcastApiProvider2 } = await Promise.resolve().then(() => (init_broadcast2(), broadcast_exports));
  return new BroadcastApiProvider2(config);
}

// src/utils/mediaPopulation.ts
async function populateMediaFields(content, payload, config) {
  if (!content || typeof content !== "object") return content;
  const typedContent = content;
  if (typedContent.root?.children) {
    for (const child of typedContent.root.children) {
      await populateBlockMediaFields(child, payload, config);
    }
  }
  return content;
}
async function populateBlockMediaFields(node, payload, config) {
  if (!node || typeof node !== "object") return;
  const typedNode = node;
  if (typedNode.type === "block" && typedNode.fields) {
    const blockType = typedNode.fields.blockType || typedNode.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 && typedNode.fields[field.name]) {
          const fieldValue = typedNode.fields[field.name];
          const collectionName = Array.isArray(field.relationTo) ? field.relationTo[0] : field.relationTo;
          if (typeof fieldValue === "string" && fieldValue.match(/^[a-f0-9]{24}$/i)) {
            try {
              const media = await payload.findByID({
                collection: collectionName,
                id: fieldValue,
                depth: 0
              });
              if (media) {
                typedNode.fields[field.name] = media;
                payload.logger?.info(
                  {
                    mediaId: fieldValue,
                    mediaUrl: media.url,
                    filename: media.filename
                  },
                  `Populated ${field.name} for block ${blockType}`
                );
              }
            } catch (error) {
              payload.logger?.error(
                { error: String(error) },
                `Failed to populate ${field.name} for block ${blockType}`
              );
            }
          }
        }
        if (field.type === "array" && field.fields) {
          const arrayValue = typedNode.fields[field.name];
          if (Array.isArray(arrayValue)) {
            for (const arrayItem of arrayValue) {
              if (arrayItem && typeof arrayItem === "object") {
                const typedArrayItem = arrayItem;
                for (const arrayField of field.fields) {
                  if (arrayField.type === "upload" && arrayField.relationTo && typedArrayItem[arrayField.name]) {
                    const arrayFieldValue = typedArrayItem[arrayField.name];
                    const arrayCollectionName = Array.isArray(arrayField.relationTo) ? arrayField.relationTo[0] : arrayField.relationTo;
                    if (typeof arrayFieldValue === "string" && arrayFieldValue.match(/^[a-f0-9]{24}$/i)) {
                      try {
                        const media = await payload.findByID({
                          collection: arrayCollectionName,
                          id: arrayFieldValue,
                          depth: 0
                        });
                        if (media) {
                          typedArrayItem[arrayField.name] = media;
                          payload.logger?.info(
                            {
                              mediaId: arrayFieldValue,
                              mediaUrl: media.url,
                              filename: media.filename
                            },
                            `Populated array ${arrayField.name} for block ${blockType}`
                          );
                        }
                      } catch (error) {
                        payload.logger?.error(
                          { error: String(error) },
                          `Failed to populate array ${arrayField.name} for block ${blockType}`
                        );
                      }
                    }
                  }
                }
              }
            }
          }
        }
        if (field.type === "richText" && typedNode.fields[field.name]) {
          await populateRichTextUploads(typedNode.fields[field.name], payload);
          payload.logger?.info(`Processed rich text field ${field.name} for upload nodes`);
        }
      }
    }
  }
  if (typedNode.children) {
    for (const child of typedNode.children) {
      await populateBlockMediaFields(child, payload, config);
    }
  }
}
async function populateRichTextUploads(content, payload) {
  if (!content || typeof content !== "object") return;
  const typedContent = content;
  if (typedContent.root?.children) {
    await processNodeArray(typedContent.root.children);
  }
  if (Array.isArray(content)) {
    await processNodeArray(content);
  }
  async function processNodeArray(nodes) {
    await Promise.all(nodes.map(processNode));
  }
  async function processNode(node) {
    if (!node || typeof node !== "object") return;
    const typedNode = node;
    if (typedNode.type === "upload" && typedNode.relationTo === "media" && typeof typedNode.value === "string" && typedNode.value.match(/^[a-f0-9]{24}$/i)) {
      try {
        const media = await payload.findByID({
          collection: "media",
          id: typedNode.value,
          depth: 0
        });
        if (media) {
          typedNode.value = media;
          payload.logger?.info(
            {
              mediaId: typedNode.value,
              mediaUrl: media.url,
              filename: media.filename
            },
            "Populated rich text upload node"
          );
        }
      } catch (error) {
        payload.logger?.error(
          { error: String(error) },
          `Failed to populate rich text upload ${typedNode.value}`
        );
      }
    }
    if (typedNode.children && Array.isArray(typedNode.children)) {
      await processNodeArray(typedNode.children);
    }
    if (typedNode.root?.children && Array.isArray(typedNode.root.children)) {
      await processNodeArray(typedNode.root.children);
    }
  }
}

// src/endpoints/broadcasts/preview.ts
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/utils/getErrorMessage.ts
init_broadcast();
function getErrorMessage(error) {
  if (error instanceof BroadcastProviderError) {
    return `${error.code}: ${error.message}`;
  }
  if (error instanceof Error) {
    return error.message;
  }
  return String(error);
}
function getErrorDetails(error) {
  if (error instanceof Error) {
    return {
      message: error.message,
      name: error.name,
      stack: error.stack,
      ...error instanceof BroadcastProviderError ? { code: error.code } : {}
    };
  }
  return { message: String(error) };
}

// src/utils/broadcast-sync.ts
function shouldSyncToProvider(doc) {
  return Boolean(doc.subject && doc.contentSection?.content);
}
async function buildProviderCreateData(doc, req, pluginConfig, providerConfig) {
  const populatedContent = await populateMediaFields(
    doc.contentSection?.content,
    req.payload,
    pluginConfig
  );
  const htmlContent = await convertToEmailSafeHtml(populatedContent, {
    wrapInTemplate: pluginConfig.customizations?.broadcasts?.emailPreview?.wrapInTemplate ?? true,
    customWrapper: pluginConfig.customizations?.broadcasts?.emailPreview?.customWrapper,
    preheader: doc.contentSection?.preheader,
    subject: doc.subject,
    documentData: doc,
    customBlockConverter: pluginConfig.customizations?.broadcasts?.customBlockConverter
  });
  return {
    name: doc.subject,
    subject: doc.subject,
    preheader: doc.contentSection?.preheader || "",
    content: htmlContent,
    trackOpens: doc.settings?.trackOpens ?? true,
    trackClicks: doc.settings?.trackClicks ?? true,
    replyTo: doc.settings?.replyTo || providerConfig?.replyTo,
    audienceIds: doc.audienceIds?.map((a) => a.audienceId) || []
  };
}
async function syncBroadcastToProvider(doc, req, pluginConfig, providerConfig, provider) {
  try {
    const createData = await buildProviderCreateData(doc, req, pluginConfig, providerConfig);
    const providerBroadcast = await provider.create(createData);
    return {
      success: true,
      providerId: providerBroadcast.id,
      externalId: providerBroadcast.id,
      providerData: providerBroadcast.providerData
    };
  } catch (error) {
    return {
      success: false,
      error: getErrorMessage(error)
    };
  }
}

// src/utils/scheduling-state.ts
init_broadcast();
function draftState() {
  return { sendStatus: "draft" /* DRAFT */, scheduledAt: null };
}
function scheduledState(scheduledAt) {
  return { sendStatus: "scheduled" /* SCHEDULED */, scheduledAt: scheduledAt.toISOString() };
}
function sendingState() {
  return { sendStatus: "sending" /* SENDING */ };
}
function failedState() {
  return { sendStatus: "failed" /* FAILED */ };
}
function detectStateTransition(previousDoc, currentDoc) {
  const wasUnpublished = !previousDoc?._status || previousDoc._status === "draft";
  const isNowPublished = currentDoc._status === "published";
  const wasScheduled = previousDoc?._status === "draft" && !!previousDoc?.publishedAt;
  const isNoLongerScheduled = currentDoc._status === "draft" && !currentDoc.publishedAt;
  const scheduleTimeChanged = previousDoc?.publishedAt !== currentDoc.publishedAt;
  const publishedAtDate = currentDoc.publishedAt ? new Date(currentDoc.publishedAt) : null;
  const isManualPublish = wasUnpublished && isNowPublished && publishedAtDate !== null && publishedAtDate.getTime() > Date.now();
  return {
    wasUnpublished,
    isNowPublished,
    wasScheduled,
    isNoLongerScheduled,
    scheduleTimeChanged,
    isManualPublish
  };
}
function areScheduledTimesEqual(a, b) {
  if (!a || !b) return false;
  return new Date(a).getTime() === b.getTime();
}
function parseScheduledDate(dateStr) {
  if (!dateStr) return null;
  const date = new Date(dateStr);
  if (Number.isNaN(date.getTime())) return null;
  return date;
}

// src/utils/idempotency.ts
var completedOperations = /* @__PURE__ */ new Map();
var DEFAULT_TTL_MS = 5 * 60 * 1e3;
function generateIdempotencyKey(broadcastId, operation, params) {
  const paramStr = params ? Object.entries(params).sort().map(([k, v]) => `${k}=${v}`).join("&") : "";
  return `${broadcastId}:${operation}:${paramStr}`;
}
function getCompletedOperation(key, ttlMs = DEFAULT_TTL_MS) {
  const entry = completedOperations.get(key);
  if (!entry) {
    return void 0;
  }
  if (Date.now() - entry.completedAt > ttlMs) {
    completedOperations.delete(key);
    return void 0;
  }
  return entry.result;
}
function markOperationCompleted(key, result) {
  completedOperations.set(key, {
    completedAt: Date.now(),
    result
  });
  if (completedOperations.size > 100) {
    cleanupExpiredEntries();
  }
}
function cleanupExpiredEntries(ttlMs = DEFAULT_TTL_MS) {
  const now = Date.now();
  for (const [key, entry] of completedOperations.entries()) {
    if (now - entry.completedAt > ttlMs) {
      completedOperations.delete(key);
    }
  }
}

// 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
init_broadcast();
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 provider = await getBroadcastProvider(req, config);
        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 BroadcastProviderError) {
          return Response.json({
            success: false,
            error: error.message,
            code: error.code
          }, { status: error.code === "CONFIGURATION_ERROR" ? 500 : 500 });
        }
        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();
init_broadcast();
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 provider = await getBroadcastProvider(req, config);
        const broadcast = await provider.schedule(broadcastDoc.providerId, scheduledDate);
        await req.payload.update({
          collection: collectionSlug,
          id,
          data: scheduledState(scheduledDate),
          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 BroadcastProviderError) {
          return Response.json({
            success: false,
            error: error.message,
            code: error.code
          }, { status: error.code === "CONFIGURATION_ERROR" ? 500 : 500 });
        }
        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 });
      }
    })
  };
};
var createCancelScheduleBroadcastEndpoint = (config, collectionSlug) => {
  return {
    path: "/:id/schedule",
    method: "delete",
    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 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 });
        }
        if (broadcastDoc.sendStatus !== "scheduled" /* SCHEDULED */) {
          return Response.json({
            success: false,
            error: `Cannot cancel: broadcast is not scheduled (current status: ${broadcastDoc.sendStatus})`
          }, { status: 400 });
        }
        const provider = await getBroadcastProvider(req, config);
        await provider.cancelSchedule(broadcastDoc.providerId);
        await req.payload.update({
          collection: collectionSlug,
          id,
          data: draftState(),
          user: auth.user
        });
        return Response.json({
          success: true,
          message: "Broadcast schedule cancelled"
        });
      } catch (error) {
        console.error("Failed to cancel scheduled 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 cancel scheduled broadcast"
        }, { status: 500 });
      }
    })
  };
};

// src/endpoints/broadcasts/retry-sync.ts
init_broadcast();
var createRetrySyncEndpoint = (config, collectionSlug) => {
  return {
    path: "/:id/retry-sync",
    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 broadcastDoc = await req.payload.findByID({
          collection: collectionSlug,
          id,
          user: auth.user
        });
        if (!broadcastDoc) {
          return Response.json({
            success: false,
            error: "Broadcast not found"
          }, { status: 404 });
        }
        const provider = await getBroadcastProvider(req, config);
        const providerConfig = await getBroadcastConfig(req, config);
        if (!broadcastDoc.providerId) {
          if (!broadcastDoc.subject || !broadcastDoc.contentSection?.content) {
            return Response.json({
              success: false,
              error: "Broadcast must have subject and content to sync"
            }, { status: 400 });
          }
          const htmlContent2 = await convertToEmailSafeHtml(
            await populateMediaFields(broadcastDoc.contentSection.content, req.payload, config),
            {
              wrapInTemplate: config.customizations?.broadcasts?.emailPreview?.wrapInTemplate ?? true,
              customWrapper: config.customizations?.broadcasts?.emailPreview?.customWrapper,
              preheader: broadcastDoc.contentSection?.preheader,
              subject: broadcastDoc.subject,
              documentData: broadcastDoc,
              customBlockConverter: config.customizations?.broadcasts?.customBlockConverter
            }
          );
          const createData = {
            name: broadcastDoc.subject,
            subject: broadcastDoc.subject,
            preheader: broadcastDoc.contentSection?.preheader || "",
            content: htmlContent2,
            trackOpens: broadcastDoc.settings?.trackOpens ?? true,
            trackClicks: broadcastDoc.settings?.trackClicks ?? true,
            replyTo: broadcastDoc.settings?.replyTo || providerConfig?.replyTo,
            audienceIds: broadcastDoc.audienceIds?.map((a) => a.audienceId) || []
          };
          const providerBroadcast = await provider.create(createData);
          await req.payload.update({
            collection: collectionSlug,
            id,
            data: {
              providerId: providerBroadcast.id,
              externalId: providerBroadcast.id,
              providerData: providerBroadcast.providerData,
              providerSyncStatus: "synced",
              providerSyncError: null,
              lastSyncAttempt: (/* @__PURE__ */ new Date()).toISOString()
            },
            user: auth.user
          });
          return Response.json({
            success: true,
            message: "Broadcast created and synced with provider"
          });
        }
        const populatedContent = await populateMediaFields(
          broadcastDoc.contentSection?.content,
          req.payload,
          config
        );
        const emailPreviewConfig = config.customizations?.broadcasts?.emailPreview;
        const htmlContent = await convertToEmailSafeHtml(populatedContent, {
          wrapInTemplate: emailPreviewConfig?.wrapInTemplate ?? true,
          customWrapper: emailPreviewConfig?.customWrapper,
          preheader: broadcastDoc.contentSection?.preheader,
          subject: broadcastDoc.subject,
          documentData: broadcastDoc,
          customBlockConverter: config.customizations?.broadcasts?.customBlockConverter
        });
        const updates = {
          name: broadcastDoc.subject,
          subject: broadcastDoc.subject,
          preheader: broadcastDoc.contentSection?.preheader,
          content: htmlContent,
          trackOpens: broadcastDoc.settings?.trackOpens,
          trackClicks: broadcastDoc.settings?.trackClicks,
          replyTo: broadcastDoc.settings?.replyTo || providerConfig?.replyTo,
          audienceIds: broadcastDoc.audienceIds?.map((a) => a.audienceId)
        };
        await provider.update(broadcastDoc.providerId, updates);
        await req.payload.update({
          collection: collectionSlug,
          id,
          data: {
            providerSyncStatus: "synced",
            providerSyncError: null,
            lastSyncAttempt: (/* @__PURE__ */ new Date()).toISOString()
          },
          user: auth.user
        });
        return Response.json({
          success: true,
          message: "Broadcast synced with provider successfully"
        });
      } catch (error) {
        console.error("Failed to retry sync:", error);
        try {
          const url = new URL(req.url || "", `http://localhost`);
          const pathParts = url.pathname.split("/");
          const id = pathParts[pathParts.length - 2];
          if (id) {
            await req.payload.update({
              collection: collectionSlug,
              id,
              data: {
                providerSyncStatus: "failed",
                providerSyncError: getErrorMessage(error),
                lastSyncAttempt: (/* @__PURE__ */ new Date()).toISOString()
              }
            });
          }
        } catch {
        }
        if (error instanceof BroadcastProviderError) {
          return Response.json({
            success: false,
            error: error.message,
            code: error.code
          }, { status: 500 });
        }
        return Response.json({
          success: false,
          error: getErrorMessage(error)
        }, { 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/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),
      createCancelScheduleBroadcastEndpoint(pluginConfig, collectionSlug),
      createRetrySyncEndpoint(pluginConfig, collectionSlug),
      createTestBroadcastEndpoint(pluginConfig, collectionSlug),
      createBroadcastPreviewEndpoint(pluginConfig, collectionSlug)
    ],
    fields: [
      {
        name: "subject",
        type: "text",
        required: true,
        admin: {
          description: "Email subject line"
        }
      },
      {
        name: "emailOnly",
        type: "checkbox",
        label: "Email Only",
        defaultValue: false,
        admin: {
          description: "Check this to send via email only (won't be published on website). Use the plugin's Schedule Send button instead of Payload's Schedule Publish."
        }
      },
      // 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"
          }
        }
      },
      // Scheduling controls - shows schedule/cancel buttons based on status
      createBroadcastScheduleField(),
      {
        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: "externalId",
        type: "text",
        admin: {
          readOnly: true,
          description: "External ID for webhook integration"
        }
      },
      {
        name: "providerData",
        type: "json",
        admin: {
          readOnly: true,
          condition: () => false
          // Hidden by default
        }
      },
      // Provider sync status tracking
      {
        name: "providerSyncStatus",
        type: "select",
        label: "Provider Sync Status",
        defaultValue: "pending",
        options: [
          { label: "Pending", value: "pending" },
          { label: "Synced", value: "synced" },
          { label: "Failed", value: "failed" }
        ],
        admin: {
          readOnly: true,
          description: "Status of sync with email provider",
          condition: (data) => hasProviders && data?.providerId,
          components: {
            Field: "payload-plugin-newsletter/components#SyncStatusField"
          }
        }
      },
      {
        name: "providerSyncError",
        type: "text",
        admin: {
          readOnly: true,
          condition: (data) => data?.providerSyncStatus === "failed",
          description: "Error message from last sync attempt"
        }
      },
      {
        name: "lastSyncAttempt",
        type: "date",
        admin: {
          readOnly: true,
          condition: () => false
          // Hidden, used for tracking
        }
      },
      // Webhook tracking fields
      {
        name: "webhookData",
        type: "group",
        label: "Webhook Data",
        admin: {
          condition: () => false
          // Hidden by default, used for webhook tracking
        },
        fields: [
          {
            name: "lastWebhookEvent",
            type: "text",
            admin: {
              readOnly: true
            }
          },
          {
            name: "lastWebhookEventAt",
            type: "date",
            admin: {
              readOnly: true
            }
          },
          {
            name: "hasWarnings",
            type: "checkbox",
            defaultValue: false
          },
          {
            name: "failureReason",
            type: "text"
          },
          {
            name: "sentCount",
            type: "number"
          },
          {
            name: "totalCount",
            type: "number"
          },
          {
            name: "failedCount",
            type: "number"
          },
          {
            name: "remainingCount",
            type: "number"
          },
          {
            name: "sendingStartedAt",
            type: "date"
          },
          {
            name: "failedAt",
            type: "date"
          },
          {
            name: "abortedAt",
            type: "date"
          },
          {
            name: "abortReason",
            type: "text"
          },
          {
            name: "pausedAt",
            type: "date"
          },
          {
            name: "webhookEvents",
            type: "array",
            label: "Webhook Event Log",
            maxRows: 10,
            admin: {
              readOnly: true,
              description: "Recent webhook events for debugging"
            },
            fields: [
              {
                name: "eventType",
                type: "text",
                admin: { readOnly: true }
              },
              {
                name: "receivedAt",
                type: "date",
                admin: { readOnly: true }
              },
              {
                name: "eventPayload",
                type: "json",
                admin: { readOnly: true }
              }
            ]
          }
        ]
      }
    ],
    hooks: {
      // Sync with provider on create and update
      afterChange: [
        async ({ doc, operation, req, previousDoc, context }) => {
          if (!hasProviders) return doc;
          const hookContext = context;
          if (hookContext?.isSchedulingUpdate) {
            return doc;
          }
          req.payload.logger.info({
            operation,
            hasProviderId: !!doc.providerId,
            hasExternalId: !!doc.externalId,
            sendStatus: doc.sendStatus,
            publishStatus: doc._status,
            hasSubject: !!doc.subject,
            hasContent: !!doc.contentSection?.content
          }, `Broadcast afterChange ${operation} hook triggered`);
          if (operation === "create") {
            if (!shouldSyncToProvider(doc)) {
              req.payload.logger.info(
                { broadcastId: doc.id },
                "Broadcast created without sufficient content - skipping provider sync"
              );
              return doc;
            }
            try {
              const provider = await getBroadcastProvider(req, pluginConfig);
              const providerConfig = await getBroadcastConfig(req, pluginConfig);
              const syncResult = await syncBroadcastToProvider(
                doc,
                req,
                pluginConfig,
                providerConfig,
                provider
              );
              if (syncResult.success) {
                req.payload.logger.info(
                  { broadcastId: doc.id, providerId: syncResult.providerId },
                  "Broadcast synced to provider on CREATE"
                );
                await req.payload.update({
                  collection: collectionSlug,
                  id: doc.id,
                  data: {
                    providerId: syncResult.providerId,
                    externalId: syncResult.externalId,
                    providerData: syncResult.providerData,
                    providerSyncStatus: "synced",
                    providerSyncError: null,
                    lastSyncAttempt: (/* @__PURE__ */ new Date()).toISOString()
                  },
                  context: { isSchedulingUpdate: true }
                });
                return {
                  ...doc,
                  providerId: syncResult.providerId,
                  externalId: syncResult.externalId,
                  providerData: syncResult.providerData,
                  providerSyncStatus: "synced",
                  providerSyncError: null
                };
              } else {
                req.payload.logger.error(
                  { broadcastId: doc.id, error: syncResult.error },
                  "Failed to sync broadcast to provider on CREATE"
                );
                await req.payload.update({
                  collection: collectionSlug,
                  id: doc.id,
                  data: {
                    providerSyncStatus: "failed",
                    providerSyncError: syncResult.error,
                    lastSyncAttempt: (/* @__PURE__ */ new Date()).toISOString()
                  },
                  context: { isSchedulingUpdate: true }
                });
                return {
                  ...doc,
                  providerSyncStatus: "failed",
                  providerSyncError: syncResult.error
                };
              }
            } catch (error) {
              req.payload.logger.error(
                { broadcastId: doc.id, error: getErrorDetails(error) },
                "Unexpected error during CREATE sync"
              );
              return doc;
            }
          }
          if (operation === "update") {
            try {
              const provider = await getBroadcastProvider(req, pluginConfig);
              const providerConfig = await getBroadcastConfig(req, pluginConfig);
              if (!doc.providerId && !doc.externalId && shouldSyncToProvider(doc)) {
                req.payload.logger.info("Creating broadcast in provider on first update with content");
                const syncResult = await syncBroadcastToProvider(
                  doc,
                  req,
                  pluginConfig,
                  providerConfig,
                  provider
                );
                if (syncResult.success) {
                  req.payload.logger.info(
                    { broadcastId: doc.id, providerId: syncResult.providerId },
                    "Broadcast synced to provider on UPDATE"
                  );
                  await req.payload.update({
                    collection: collectionSlug,
                    id: doc.id,
                    data: {
                      providerId: syncResult.providerId,
                      externalId: syncResult.externalId,
                      providerData: syncResult.providerData,
                      providerSyncStatus: "synced",
                      providerSyncError: null,
                      lastSyncAttempt: (/* @__PURE__ */ new Date()).toISOString()
                    },
                    context: { isSchedulingUpdate: true }
                  });
                  return {
                    ...doc,
                    providerId: syncResult.providerId,
                    externalId: syncResult.externalId,
                    providerData: syncResult.providerData,
                    providerSyncStatus: "synced",
                    providerSyncError: null
                  };
                } else {
                  req.payload.logger.error(
                    { broadcastId: doc.id, error: syncResult.error },
                    "Failed to create broadcast in provider during UPDATE"
                  );
                }
              }
              if (!doc.providerId) {
                req.payload.logger.info(`Broadcast ${doc.id} has no providerId and insufficient content for creation - skipping sync`);
                return doc;
              }
              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({
                    providerId: doc.providerId,
                    updates
                  }, "Syncing broadcast updates to provider");
                  await provider.update(doc.providerId, updates);
                  await req.payload.update({
                    collection: collectionSlug,
                    id: doc.id,
                    data: {
                      providerSyncStatus: "synced",
                      providerSyncError: null,
                      lastSyncAttempt: (/* @__PURE__ */ new Date()).toISOString()
                    },
                    context: { isSchedulingUpdate: true }
                  });
                  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(
                {
                  broadcastId: doc.id,
                  subject: doc.subject,
                  error: getErrorDetails(error)
                },
                "Failed to sync broadcast to provider"
              );
              try {
                await req.payload.update({
                  collection: collectionSlug,
                  id: doc.id,
                  data: {
                    providerSyncStatus: "failed",
                    providerSyncError: getErrorMessage(error),
                    lastSyncAttempt: (/* @__PURE__ */ new Date()).toISOString()
                  },
                  context: { isSchedulingUpdate: true }
                });
              } catch (updateError) {
                req.payload.logger.error(
                  { broadcastId: doc.id, error: getErrorDetails(updateError) },
                  "Failed to update sync status after sync failure"
                );
              }
            }
          }
          return doc;
        },
        // Unified scheduling hook: syncs email scheduling with Payload publish scheduling
        async ({ doc, operation, previousDoc, req, context }) => {
          const hookContext = context;
          if (hookContext?.isSchedulingUpdate) {
            return doc;
          }
          if (operation !== "update") {
            return doc;
          }
          if (!hasProviders) {
            return doc;
          }
          const broadcastDoc = doc;
          if (!broadcastDoc.providerId) {
            return doc;
          }
          if (broadcastDoc.emailOnly === true) {
            return doc;
          }
          if (broadcastDoc.sendStatus === "sent" /* SENT */ || broadcastDoc.sendStatus === "sending" /* SENDING */) {
            return doc;
          }
          const transition = detectStateTransition(
            previousDoc,
            broadcastDoc
          );
          const publishedAt = parseScheduledDate(broadcastDoc.publishedAt);
          const now = /* @__PURE__ */ new Date();
          if (broadcastDoc.sendStatus === "scheduled" /* SCHEDULED */ && areScheduledTimesEqual(broadcastDoc.scheduledAt, publishedAt)) {
            return doc;
          }
          if (transition.wasUnpublished && transition.isNowPublished) {
            try {
              const provider = await getBroadcastProvider(req, pluginConfig);
              if (transition.isManualPublish) {
                req.payload.logger.info(
                  {
                    broadcastId: broadcastDoc.id,
                    action: "manualPublishOverride",
                    scheduledFor: broadcastDoc.publishedAt
                  },
                  "Manual publish detected, sending immediately instead of waiting for scheduled time"
                );
              }
              if (publishedAt && publishedAt.getTime() > now.getTime() && !transition.isManualPublish) {
                const scheduleKey = generateIdempotencyKey(broadcastDoc.id, "schedule", {
                  time: publishedAt.getTime()
                });
                const cached = getCompletedOperation(scheduleKey);
                if (cached) {
                  req.payload.logger.info(
                    { broadcastId: broadcastDoc.id, idempotencyKey: scheduleKey },
                    "Schedule operation already completed (idempotency check), updating local state only"
                  );
                } else {
                  await provider.schedule(broadcastDoc.providerId, publishedAt);
                  markOperationCompleted(scheduleKey, { scheduledAt: publishedAt.toISOString() });
                }
                await req.payload.update({
                  collection: "broadcasts",
                  id: broadcastDoc.id,
                  data: scheduledState(publishedAt),
                  context: { isSchedulingUpdate: true }
                });
                req.payload.logger.info(
                  {
                    broadcastId: broadcastDoc.id,
                    scheduledAt: publishedAt.toISOString()
                  },
                  "Email scheduled to sync with website publish"
                );
              } else {
                const sendKey = generateIdempotencyKey(broadcastDoc.id, "send", {});
                const cached = getCompletedOperation(sendKey);
                if (cached) {
                  req.payload.logger.info(
                    { broadcastId: broadcastDoc.id, idempotencyKey: sendKey },
                    "Send operation already completed (idempotency check), updating local state only"
                  );
                } else {
                  await provider.send(broadcastDoc.providerId);
                  markOperationCompleted(sendKey, { sentAt: (/* @__PURE__ */ new Date()).toISOString() });
                }
                await req.payload.update({
                  collection: "broadcasts",
                  id: broadcastDoc.id,
                  data: {
                    ...sendingState(),
                    sentAt: (/* @__PURE__ */ new Date()).toISOString()
                  },
                  context: { isSchedulingUpdate: true }
                });
                req.payload.logger.info(
                  { broadcastId: broadcastDoc.id },
                  "Broadcast sent successfully on publish"
                );
              }
            } catch (error) {
              req.payload.logger.error(
                {
                  broadcastId: broadcastDoc.id,
                  error: getErrorDetails(error)
                },
                "Failed to schedule/send email for published broadcast"
              );
              await req.payload.update({
                collection: "broadcasts",
                id: broadcastDoc.id,
                data: {
                  ...failedState(),
                  // Store error in webhookData.failureReason since we don't have a sendError field
                  webhookData: {
                    ...doc.webhookData || {},
                    failureReason: `Email operation failed: ${getErrorMessage(error)}`
                  }
                },
                context: { isSchedulingUpdate: true }
              });
            }
            return doc;
          }
          if (transition.wasScheduled && transition.isNoLongerScheduled && broadcastDoc.sendStatus === "scheduled" /* SCHEDULED */) {
            try {
              const provider = await getBroadcastProvider(req, pluginConfig);
              const cancelKey = generateIdempotencyKey(broadcastDoc.id, "cancel", {});
              const cached = getCompletedOperation(cancelKey);
              if (cached) {
                req.payload.logger.info(
                  { broadcastId: broadcastDoc.id, idempotencyKey: cancelKey },
                  "Cancel operation already completed (idempotency check), updating local state only"
                );
              } else {
                await provider.cancelSchedule(broadcastDoc.providerId);
                markOperationCompleted(cancelKey, { cancelledAt: (/* @__PURE__ */ new Date()).toISOString() });
              }
              await req.payload.update({
                collection: "broadcasts",
                id: broadcastDoc.id,
                data: draftState(),
                context: { isSchedulingUpdate: true }
              });
              req.payload.logger.info(
                { broadcastId: broadcastDoc.id },
                "Email schedule cancelled (publish schedule removed)"
              );
            } catch (error) {
              req.payload.logger.error(
                {
                  broadcastId: broadcastDoc.id,
                  error: getErrorDetails(error)
                },
                "Failed to cancel scheduled email - email may still send at original time"
              );
            }
            return doc;
          }
          if (transition.scheduleTimeChanged && publishedAt && broadcastDoc.sendStatus === "scheduled" /* SCHEDULED */) {
            try {
              const provider = await getBroadcastProvider(req, pluginConfig);
              const previousScheduledAt = broadcastDoc.scheduledAt;
              const rescheduleKey = generateIdempotencyKey(broadcastDoc.id, "schedule", {
                from: previousScheduledAt ? new Date(previousScheduledAt).getTime() : 0,
                to: publishedAt.getTime()
              });
              const cached = getCompletedOperation(rescheduleKey);
              if (cached) {
                req.payload.logger.info(
                  { broadcastId: broadcastDoc.id, idempotencyKey: rescheduleKey },
                  "Reschedule operation already completed (idempotency check), updating local state only"
                );
              } else {
                await provider.cancelSchedule(broadcastDoc.providerId);
                await provider.schedule(broadcastDoc.providerId, publishedAt);
                markOperationCompleted(rescheduleKey, {
                  rescheduledAt: (/* @__PURE__ */ new Date()).toISOString(),
                  newTime: publishedAt.toISOString()
                });
              }
              await req.payload.update({
                collection: "broadcasts",
                id: broadcastDoc.id,
                data: {
                  scheduledAt: publishedAt.toISOString()
                },
                context: { isSchedulingUpdate: true }
              });
              req.payload.logger.info(
                {
                  broadcastId: broadcastDoc.id,
                  newScheduledAt: publishedAt.toISOString()
                },
                "Email rescheduled to new publish time"
              );
            } catch (error) {
              req.payload.logger.error(
                {
                  broadcastId: broadcastDoc.id,
                  newScheduledAt: publishedAt.toISOString(),
                  error: getErrorDetails(error)
                },
                "Failed to reschedule email - may send at original time or not at all"
              );
            }
            return doc;
          }
          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 provider = await getBroadcastProvider(req, pluginConfig);
            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({ error: String(error) }, "Failed to delete broadcast from provider");
            }
          }
          return doc;
        }
      ]
    }
  };
};

// src/emails/render.tsx
var import_render = require("@react-email/render");

// src/emails/MagicLink.tsx
var import_components = require("@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
var import_jsx_runtime = require("react/jsx-runtime");
var MagicLinkEmail = ({
  magicLink,
  email,
  siteName = "Newsletter",
  expiresIn = "24 hours"
}) => {
  const previewText = `Sign in to ${siteName}`;
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_components.Html, { children: [
    /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_components.Head, {}),
    /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_components.Preview, { children: previewText }),
    /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_components.Body, { style: styles.main, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_components.Container, { style: styles.container, children: [
      /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_components.Text, { style: styles.heading, children: [
        "Sign in to ",
        siteName
      ] }),
      /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_components.Text, { style: styles.text, children: [
        "Hi ",
        email.split("@")[0],
        ","
      ] }),
      /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_components.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__ */ (0, import_jsx_runtime.jsxs)(import_components.Button, { href: magicLink, style: styles.button, children: [
        "Sign in to ",
        siteName
      ] }),
      /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_components.Text, { style: styles.text, children: "Or copy and paste this URL into your browser:" }),
      /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { style: styles.code, children: magicLink }),
      /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_components.Hr, { style: styles.hr }),
      /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_components.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
var import_components2 = require("@react-email/components");
var import_jsx_runtime2 = require("react/jsx-runtime");
var WelcomeEmail = ({
  email,
  siteName = "Newsletter",
  preferencesUrl
}) => {
  const previewText = `Welcome to ${siteName}!`;
  const firstName = email.split("@")[0];
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_components2.Html, { children: [
    /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_components2.Head, {}),
    /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_components2.Preview, { children: previewText }),
    /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_components2.Body, { style: styles.main, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_components2.Container, { style: styles.container, children: [
      /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_components2.Text, { style: styles.heading, children: [
        "Welcome to ",
        siteName,
        "! \u{1F389}"
      ] }),
      /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_components2.Text, { style: styles.text, children: [
        "Hi ",
        firstName,
        ","
      ] }),
      /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_components2.Text, { style: styles.text, children: [
        "Thanks for subscribing to ",
        siteName,
        "! We're excited to have you as part of our community."
      ] }),
      /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_components2.Text, { style: styles.text, children: "You'll receive our newsletter based on your preferences. Speaking of which, you can update your preferences anytime:" }),
      preferencesUrl && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_components2.Button, { href: preferencesUrl, style: styles.button, children: "Manage Preferences" }),
      /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_components2.Text, { style: styles.text, children: "Here's what you can expect from us:" }),
      /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_components2.Text, { style: styles.text, children: [
        "\u2022 Regular updates based on your chosen frequency",
        /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("br", {}),
        "\u2022 Content tailored to your interests",
        /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("br", {}),
        "\u2022 Easy unsubscribe options in every email",
        /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("br", {}),
        "\u2022 Your privacy respected always"
      ] }),
      /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_components2.Hr, { style: styles.hr }),
      /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_components2.Text, { 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
var import_jsx_runtime3 = require("react/jsx-runtime");
var SignInEmail = (props) => {
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MagicLinkEmail, { ...props });
};

// src/emails/render.tsx
var import_jsx_runtime4 = require("react/jsx-runtime");
async function renderEmail(template, data, config) {
  try {
    if (config?.customTemplates) {
      const customTemplate = config.customTemplates[template];
      if (customTemplate) {
        const CustomComponent = customTemplate;
        return (0, import_render.render)(/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(CustomComponent, { ...data }));
      }
    }
    switch (template) {
      case "magic-link": {
        const magicLinkData = data;
        return (0, import_render.render)(
          /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
            MagicLinkEmail,
            {
              magicLink: magicLinkData.magicLink || magicLinkData.verificationUrl || magicLinkData.magicLinkUrl || "",
              email: magicLinkData.email || "",
              siteName: magicLinkData.siteName,
              expiresIn: magicLinkData.expiresIn
            }
          )
        );
      }
      case "signin": {
        const signinData = data;
        return (0, import_render.render)(
          /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
            SignInEmail,
            {
              magicLink: signinData.magicLink || signinData.verificationUrl || signinData.magicLinkUrl || "",
              email: signinData.email || "",
              siteName: signinData.siteName,
              expiresIn: signinData.expiresIn
            }
          )
        );
      }
      case "welcome": {
        const welcomeData = data;
        return (0, import_render.render)(
          /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
            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
    },
    // External ID for webhook integration
    {
      name: "externalId",
      type: "text",
      admin: {
        description: "ID from email service provider",
        readOnly: 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: "subscribedAt",
      type: "date",
      admin: {
        description: "When the user subscribed",
        readOnly: true
      }
    },
    {
      name: "unsubscribedAt",
      type: "date",
      admin: {
        condition: (data) => data?.subscriptionStatus === "unsubscribed",
        description: "When the user unsubscribed",
        readOnly: true
      }
    },
    {
      name: "unsubscribeReason",
      type: "text",
      admin: {
        condition: (data) => data?.subscriptionStatus === "unsubscribed",
        description: "Reason for unsubscribing",
        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"
      }
    },
    // Import tracking
    {
      name: "importedFromProvider",
      type: "checkbox",
      defaultValue: false,
      admin: {
        description: "Indicates this subscriber was imported from an external provider via webhook",
        position: "sidebar",
        readOnly: true
      }
    }
  ];
  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 && !doc.importedFromProvider) {
              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 === "active" && previousDoc.subscriptionStatus === "unsubscribed" && !doc.importedFromProvider && 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`
                }, pluginConfig);
                await emailService.send({
                  to: doc.email,
                  subject: settings?.brandSettings?.siteName ? `Welcome back to ${settings.brandSettings.siteName}!` : "Welcome back!",
                  html
                });
                console.warn(`Welcome email sent to resubscribed user: ${doc.email}`);
              } catch (error) {
                console.error("Failed to send resubscription welcome email:", error);
              }
              if (pluginConfig.hooks?.afterSubscribe) {
                await pluginConfig.hooks.afterSubscribe({ doc, req });
              }
            }
            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;
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
  createBroadcastsCollection,
  createSubscribersCollection
});
//# sourceMappingURL=collections.cjs.map