UNPKG

waitlist-mailer

Version:

Modern, modular TypeScript library for managing waitlists with pluggable storage and mail providers. Supports MongoDB, SQL databases, and custom adapters with zero required dependencies for basic usage.

1,086 lines (1,076 loc) 34.3 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 __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); // lib/index.ts var index_exports = {}; __export(index_exports, { ConsoleMailProvider: () => ConsoleMailProvider, MemoryStorage: () => MemoryStorage, MongooseStorage: () => MongooseStorage, NodemailerProvider: () => NodemailerProvider, SequelizeStorage: () => SequelizeStorage, VERSION: () => VERSION, WaitlistManager: () => WaitlistManager }); module.exports = __toCommonJS(index_exports); // lib/core/WaitlistManager.ts var import_events = require("events"); var import_validator = __toESM(require("validator")); // lib/adapters/storage/MemoryStorage.ts var MemoryStorage = class { constructor() { this.storage = /* @__PURE__ */ new Map(); } /** * Add an email to memory storage. * @param email - The email to add * @param data - Optional metadata * @throws {Error} If the email already exists */ async add(email, data) { const normalized = email.toLowerCase(); if (this.storage.has(normalized)) { throw new Error(`Email already exists: ${email}`); } this.storage.set(normalized, { email, metadata: data, createdAt: /* @__PURE__ */ new Date() }); } /** * Remove an email from memory storage. * @param email - The email to remove * @returns true if the email was found and removed */ async remove(email) { return this.storage.delete(email.toLowerCase()); } /** * Check if an email exists in storage. * @param email - The email to check * @returns true if the email exists */ async exists(email) { return this.storage.has(email.toLowerCase()); } /** * Retrieve all entries from storage. * @returns Array of all WaitlistEntry objects */ async getAll() { return Array.from(this.storage.values()); } /** * Count total entries in storage. * @returns Number of entries */ async count() { return this.storage.size; } /** * Clear all entries from storage. */ async clear() { this.storage.clear(); } /** * Search entries by email pattern. * For MemoryStorage, this filters the in-memory Map. * @param pattern - The pattern to search for * @param options - Optional search options * @returns Matching entries */ async search(pattern, options = {}) { const { limit, offset = 0, caseInsensitive = true } = options; const searchPattern = caseInsensitive ? pattern.toLowerCase() : pattern; let results = []; for (const entry of this.storage.values()) { const emailToMatch = caseInsensitive ? entry.email.toLowerCase() : entry.email; if (emailToMatch.includes(searchPattern)) { results.push(entry); } } if (offset > 0) { results = results.slice(offset); } if (limit !== void 0 && limit > 0) { results = results.slice(0, limit); } return results; } /** * Iterate over entries in batches. * For MemoryStorage, yields entries from the in-memory Map. * @param batchSize - Number of entries per batch (default: 100) */ async *iterate(batchSize = 100) { const entries = Array.from(this.storage.values()); for (let i = 0; i < entries.length; i += batchSize) { const batch = entries.slice(i, i + batchSize); for (const entry of batch) { yield entry; } } } }; // lib/core/WaitlistManager.ts var WaitlistManager = class extends import_events.EventEmitter { /** * Create a new WaitlistManager instance. * @param config - Configuration object with storage, optional mailer, etc. */ constructor(config) { super(); this.initialized = false; this.storage = config.storage || new MemoryStorage(); this.mailer = config.mailer; this.companyName = config.companyName || "Our Platform"; if (config.autoConnect !== false) { this.connect().catch((error) => { console.error("Failed to auto-connect:", error); this.emit("error", { context: "autoConnect", error }); }); } } /** * Initialize connections to storage and mail providers. * @returns Promise that resolves when initialization is complete */ async connect() { try { if (this.storage.connect) { await this.storage.connect(); this.emit("storage:connected"); } if (this.mailer?.verify) { const verified = await this.mailer.verify(); if (verified) { this.emit("mailer:verified"); } else { console.warn("Mail provider verification failed"); this.emit("mailer:verification-failed"); } } this.initialized = true; this.emit("initialized"); } catch (error) { this.emit("error", { context: "connect", error }); throw error; } } /** * Wait for the manager to be fully initialized. * @returns Promise that resolves when initialized */ async waitForInitialization() { if (this.initialized) return; return new Promise((resolve) => this.once("initialized", resolve)); } /** * Check if the manager is initialized. * @returns true if initialized */ isInitialized() { return this.initialized; } /** * Add an email to the waitlist. * * Validates the email and checks for duplicates before adding. * Optionally sends a confirmation email. * * @param email - The email address to add * @param metadata - Optional custom metadata * @returns Result object with success status and message * * @example * ```typescript * const result = await manager.join('user@example.com', { * name: 'Alice', * referralSource: 'ProductHunt' * }); * * if (result.success) { * console.log('User added:', result.email); * } * ``` */ async join(email, metadata) { try { if (!import_validator.default.isEmail(email)) { this.emit("join:validation-error", { email, reason: "invalid-format" }); return { success: false, message: "Invalid email format", email }; } const normalizedEmail = email.toLowerCase(); const exists = await this.storage.exists(normalizedEmail); if (exists) { this.emit("join:duplicate", { email: normalizedEmail, metadata }); return { success: false, message: "Email already in waitlist", email: normalizedEmail }; } await this.storage.add(normalizedEmail, metadata); this.emit("join:success", { email: normalizedEmail, metadata }); if (this.mailer) { try { const context = { email: normalizedEmail, companyName: this.companyName, ...metadata }; const sent = await this.mailer.sendConfirmation(normalizedEmail, context); if (sent) { this.emit("mailer:sent", { email: normalizedEmail }); } else { this.emit("mailer:failed", { email: normalizedEmail }); } } catch (error) { console.error("Mail sending error:", error); this.emit("mailer:error", { email: normalizedEmail, error }); } } return { success: true, message: "Successfully joined the waitlist", email: normalizedEmail }; } catch (error) { this.emit("join:error", { email, error }); return { success: false, message: "Failed to add email to waitlist", email }; } } /** * Remove an email from the waitlist. * @param email - The email address to remove * @returns true if the email was found and removed */ async leave(email) { try { const normalizedEmail = email.toLowerCase(); const removed = await this.storage.remove(normalizedEmail); if (removed) { this.emit("leave:success", { email: normalizedEmail }); } else { this.emit("leave:not-found", { email: normalizedEmail }); } return removed; } catch (error) { this.emit("leave:error", { email, error }); return false; } } /** * Check if an email is in the waitlist. * @param email - The email to check * @returns true if the email exists */ async has(email) { return this.storage.exists(email.toLowerCase()); } /** * Get all waitlist entries. * @returns Array of WaitlistEntry objects */ async getAll() { return this.storage.getAll(); } /** * Get count of waitlist entries. * @returns Number of entries */ async count() { return this.storage.count(); } /** * Clear all entries from the waitlist. * @returns true if successful */ async clear() { try { await this.storage.clear(); this.emit("clear:success"); return true; } catch (error) { this.emit("clear:error", { error }); return false; } } /** * Send a custom email to an address. * Useful for sending emails to users who aren't on the waitlist. * * @param email - Recipient email * @param context - Email context with template variables * @returns true if sent successfully */ async sendEmail(email, context) { if (!this.mailer) { console.warn("No mail provider configured"); return false; } try { const normalizedEmail = email.toLowerCase(); const fullContext = { companyName: this.companyName, ...context, email: normalizedEmail }; return await this.mailer.sendConfirmation(normalizedEmail, fullContext); } catch (error) { this.emit("email:error", { email, error }); return false; } } /** * Send emails to all waitlist members. * Processes emails concurrently with configurable concurrency limit. * Uses streaming when available to avoid loading all entries into memory. * * @param contextBuilder - Function to build context for each email * @param concurrency - Maximum concurrent emails to send (1 = sequential, Infinity = all at once) * Default: 5 for balance between speed and resource usage * @returns Object with statistics * * @example * ```typescript * // Send with default concurrency (5 at a time) * const result = await manager.sendBulkEmails((email) => ({ * subject: 'Welcome!' * })); * * // Send slowly (1 at a time) * const result = await manager.sendBulkEmails( * (email) => ({ subject: 'Welcome!' }), * 1 * ); * * // Send fast (all at once - use with caution) * const result = await manager.sendBulkEmails( * (email) => ({ subject: 'Welcome!' }), * Infinity * ); * ``` */ async sendBulkEmails(contextBuilder, concurrency = 5) { if (!this.mailer) { console.warn("No mail provider configured"); return { sent: 0, failed: 0, total: 0 }; } if (this.storage.iterate) { return this.sendBulkEmailsWithStreaming(contextBuilder, concurrency); } console.warn( "Storage provider does not implement iterate(). Loading all entries into memory. This may cause performance issues with large datasets." ); return this.sendBulkEmailsLegacy(contextBuilder, concurrency); } /** * Send bulk emails using streaming to avoid memory issues. * Processes entries in batches using the storage iterator. * @private */ async sendBulkEmailsWithStreaming(contextBuilder, concurrency) { let sent = 0; let failed = 0; let total = 0; const activePromises = []; for await (const entry of this.storage.iterate(100)) { total++; if (concurrency !== Infinity && activePromises.length >= concurrency) { await Promise.race(activePromises); } const promise = (async () => { const success = await this.sendSingleBulkEmail(entry, contextBuilder); if (success) sent++; else failed++; })(); if (concurrency === 1) { await promise; } else { const trackedPromise = promise.then(() => { const index = activePromises.indexOf(trackedPromise); if (index > -1) activePromises.splice(index, 1); }); activePromises.push(trackedPromise); } } await Promise.all(activePromises); const result = { sent, failed, total }; this.emit("bulk:complete", result); return result; } /** * Legacy bulk email method that loads all entries into memory. * Used as fallback when storage doesn't support streaming. * @private */ async sendBulkEmailsLegacy(contextBuilder, concurrency) { const entries = await this.storage.getAll(); if (entries.length === 0) { this.emit("bulk:complete", { sent: 0, failed: 0, total: 0 }); return { sent: 0, failed: 0, total: 0 }; } let sent = 0; let failed = 0; if (concurrency === 1) { for (const entry of entries) { const success = await this.sendSingleBulkEmail(entry, contextBuilder); if (success) sent++; else failed++; } } else if (concurrency === Infinity) { const promises = entries.map( (entry) => this.sendSingleBulkEmail(entry, contextBuilder) ); const results = await Promise.all(promises); sent = results.filter((s) => s).length; failed = results.filter((s) => !s).length; } else { const activePromises = []; for (const entry of entries) { if (activePromises.length >= concurrency) { await Promise.race(activePromises); } const promise = this.sendSingleBulkEmail(entry, contextBuilder); activePromises.push(promise); promise.then((success) => { const index = activePromises.indexOf(promise); if (index > -1) { activePromises.splice(index, 1); } if (success) sent++; else failed++; }); } await Promise.all(activePromises); } const result = { sent, failed, total: entries.length }; this.emit("bulk:complete", result); return result; } /** * Internal method to send a single bulk email. * @private */ async sendSingleBulkEmail(entry, contextBuilder) { try { const context = { companyName: this.companyName, ...contextBuilder(entry.email, entry.metadata), email: entry.email }; const result = await this.mailer.sendConfirmation(entry.email, context); if (result) { this.emit("bulk:email-sent", { email: entry.email }); } else { this.emit("bulk:email-failed", { email: entry.email }); } return result; } catch (error) { this.emit("bulk:email-error", { email: entry.email, error }); return false; } } /** * Get entries matching a search pattern. * Delegates to storage provider for efficient database-level filtering when available. * Falls back to in-memory filtering if storage doesn't support search. * * @param pattern - Pattern to search for (partial email match) * @param options - Optional search options (limit, offset, caseInsensitive) * @returns Matching entries * * @example * ```typescript * // Simple search * const gmailUsers = await manager.search('gmail.com'); * * // Paginated search * const page1 = await manager.search('example', { limit: 10, offset: 0 }); * const page2 = await manager.search('example', { limit: 10, offset: 10 }); * ``` */ async search(pattern, options = {}) { if (this.storage.search) { return this.storage.search(pattern, options); } console.warn( "Storage provider does not implement search(). Falling back to in-memory filtering. This may cause performance issues with large datasets." ); const { limit, offset = 0, caseInsensitive = true } = options; const entries = await this.storage.getAll(); const searchPattern = caseInsensitive ? pattern.toLowerCase() : pattern; let results = entries.filter((entry) => { const emailToMatch = caseInsensitive ? entry.email.toLowerCase() : entry.email; return emailToMatch.includes(searchPattern); }); if (offset > 0) { results = results.slice(offset); } if (limit !== void 0 && limit > 0) { results = results.slice(0, limit); } return results; } /** * Disconnect from storage and mail providers. * Should be called before shutting down the application. */ async disconnect() { try { if (this.storage.disconnect) { await this.storage.disconnect(); this.emit("storage:disconnected"); } this.initialized = false; this.emit("disconnected"); } catch (error) { this.emit("error", { context: "disconnect", error }); throw error; } } }; // lib/adapters/storage/MongooseStorage.ts var MongooseStorage = class { /** * Create a new MongooseStorage instance. * @param config - Configuration object with mongoose model * @throws {Error} If mongoose is not installed or model is invalid */ constructor(config) { try { if (!config.model) { throw new Error("Mongoose model is required. Please provide a valid Mongoose model instance."); } if (!config.model.collection) { throw new Error("Invalid Mongoose model. Make sure you're passing a compiled Mongoose model."); } } catch (error) { if (error.message.includes("Mongoose") || error.message.includes("model")) { throw error; } throw new Error( "MongooseStorage requires mongoose to be installed. Install it with: npm install mongoose" ); } this.model = config.model; this.normalizeEmail = config.normalizeEmail ?? true; } normalizeEmailAddress(email) { return this.normalizeEmail ? email.toLowerCase() : email; } /** * Add an email to MongoDB. * @param email - The email to add * @param data - Optional metadata * @throws {Error} If the email already exists or database operation fails */ async add(email, data) { const normalized = this.normalizeEmailAddress(email); const existingDoc = await this.model.findOne({ email: normalized }).exec(); if (existingDoc) { throw new Error(`Email already exists: ${email}`); } try { await this.model.create({ email: normalized, metadata: data, createdAt: /* @__PURE__ */ new Date() }); } catch (error) { if (error.code === 11e3) { throw new Error(`Email already exists: ${email}`); } throw error; } } /** * Remove an email from MongoDB. * @param email - The email to remove * @returns true if the email was found and removed */ async remove(email) { const normalized = this.normalizeEmailAddress(email); const result = await this.model.deleteOne({ email: normalized }).exec(); return result.deletedCount > 0; } /** * Check if an email exists in MongoDB. * @param email - The email to check * @returns true if the email exists */ async exists(email) { const normalized = this.normalizeEmailAddress(email); const count = await this.model.countDocuments({ email: normalized }).exec(); return count > 0; } /** * Retrieve all entries from MongoDB. * @returns Array of all WaitlistEntry objects */ async getAll() { const docs = await this.model.find().lean().exec(); return docs.map((doc) => ({ email: doc.email, metadata: doc.metadata, createdAt: doc.createdAt })); } /** * Count total entries in MongoDB. * @returns Number of entries */ async count() { return this.model.countDocuments().exec(); } /** * Clear all entries from MongoDB. */ async clear() { await this.model.deleteMany({}).exec(); } /** * Search entries by email pattern using MongoDB $regex. * Delegates filtering to MongoDB for optimal performance. * @param pattern - The pattern to search for * @param options - Optional search options * @returns Matching entries */ async search(pattern, options = {}) { const { limit, offset = 0, caseInsensitive = true } = options; const escapedPattern = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const regexOptions = caseInsensitive ? "i" : ""; let query = this.model.find({ email: { $regex: escapedPattern, $options: regexOptions } }); if (offset > 0) { query = query.skip(offset); } if (limit !== void 0 && limit > 0) { query = query.limit(limit); } const docs = await query.lean().exec(); return docs.map((doc) => ({ email: doc.email, metadata: doc.metadata, createdAt: doc.createdAt })); } /** * Iterate over entries using MongoDB cursor for memory-efficient streaming. * Uses Mongoose cursor to avoid loading all documents into memory. * @param batchSize - Number of entries per batch (default: 100) */ async *iterate(batchSize = 100) { const cursor = this.model.find().lean().batchSize(batchSize).cursor(); for await (const doc of cursor) { yield { email: doc.email, metadata: doc.metadata, createdAt: doc.createdAt }; } } }; // lib/adapters/storage/SequelizeStorage.ts var SequelizeStorage = class { /** * Create a new SequelizeStorage instance. * @param config - Configuration object with sequelize model * @throws {Error} If sequelize is not installed or model is invalid */ constructor(config) { try { if (!config.model) { throw new Error("Sequelize model is required. Please provide a valid Sequelize model instance."); } if (!config.model.sequelize) { throw new Error("Invalid Sequelize model. Make sure you're passing a compiled Sequelize model."); } if (!config.model.findOne || !config.model.create) { throw new Error("Invalid Sequelize model. Model must have findOne and create methods."); } } catch (error) { if (error.message.includes("Sequelize") || error.message.includes("model")) { throw error; } throw new Error( "SequelizeStorage requires sequelize to be installed. Install it with: npm install sequelize pg (for PostgreSQL) or npm install sequelize mysql2 (for MySQL) or npm install sequelize better-sqlite3 (for SQLite)" ); } this.model = config.model; this.normalizeEmail = config.normalizeEmail ?? true; } normalizeEmailAddress(email) { return this.normalizeEmail ? email.toLowerCase() : email; } /** * Add an email to the database. * @param email - The email to add * @param data - Optional metadata * @throws {Error} If the email already exists or database operation fails */ async add(email, data) { const normalized = this.normalizeEmailAddress(email); const existing = await this.model.findOne({ where: { email: normalized } }); if (existing) { throw new Error(`Email already exists: ${email}`); } try { await this.model.create({ email: normalized, metadata: data ? JSON.stringify(data) : null, createdAt: /* @__PURE__ */ new Date() }); } catch (error) { if (error.name === "SequelizeUniqueConstraintError") { throw new Error(`Email already exists: ${email}`); } throw error; } } /** * Remove an email from the database. * @param email - The email to remove * @returns true if the email was found and removed */ async remove(email) { const normalized = this.normalizeEmailAddress(email); const result = await this.model.destroy({ where: { email: normalized } }); return result > 0; } /** * Check if an email exists in the database. * @param email - The email to check * @returns true if the email exists */ async exists(email) { const normalized = this.normalizeEmailAddress(email); const count = await this.model.count({ where: { email: normalized } }); return count > 0; } /** * Retrieve all entries from the database. * @returns Array of all WaitlistEntry objects */ async getAll() { const records = await this.model.findAll({ raw: true }); return records.map((record) => ({ email: record.email, metadata: this.safeParseMetadata(record.metadata), createdAt: record.createdAt })); } /** * Count total entries in the database. * @returns Number of entries */ async count() { return this.model.count(); } /** * Clear all entries from the database. */ async clear() { await this.model.destroy({ where: {} }); } /** * Search entries by email pattern using SQL LIKE/ILIKE. * Delegates filtering to the database for optimal performance. * @param pattern - The pattern to search for * @param options - Optional search options * @returns Matching entries */ async search(pattern, options = {}) { const { limit, offset = 0, caseInsensitive = true } = options; const { Op } = this.model.sequelize.Sequelize; const escapedPattern = pattern.replace(/[%_\\]/g, "\\$&"); const likePattern = `%${escapedPattern}%`; const dialect = this.model.sequelize.getDialect(); let whereClause; if (caseInsensitive) { if (dialect === "postgres") { whereClause = { email: { [Op.iLike]: likePattern } }; } else { whereClause = this.model.sequelize.where( this.model.sequelize.fn("LOWER", this.model.sequelize.col("email")), { [Op.like]: likePattern.toLowerCase() } ); } } else { whereClause = { email: { [Op.like]: likePattern } }; } const queryOptions = { where: whereClause, raw: true }; if (offset > 0) { queryOptions.offset = offset; } if (limit !== void 0 && limit > 0) { queryOptions.limit = limit; } const records = await this.model.findAll(queryOptions); return records.map((record) => ({ email: record.email, metadata: this.safeParseMetadata(record.metadata), createdAt: record.createdAt })); } /** * Safely parse metadata JSON with validation. * Prevents potential issues from malformed or malicious JSON. * @param metadata - Raw metadata string from database * @returns Parsed metadata or undefined */ safeParseMetadata(metadata) { if (!metadata) return void 0; try { const parsed = JSON.parse(metadata); if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) { return parsed; } console.warn("Metadata is not a plain object, ignoring"); return void 0; } catch (error) { console.error("Failed to parse metadata JSON:", error); return void 0; } } /** * Iterate over entries using Sequelize pagination for memory-efficient streaming. * Uses limit/offset pagination to avoid loading all records into memory. * @param batchSize - Number of entries per batch (default: 100) */ async *iterate(batchSize = 100) { let offset = 0; let hasMore = true; while (hasMore) { const records = await this.model.findAll({ raw: true, limit: batchSize, offset, order: [["createdAt", "ASC"]] // Consistent ordering for pagination }); if (records.length === 0) { hasMore = false; } else { for (const record of records) { yield { email: record.email, metadata: this.safeParseMetadata(record.metadata), createdAt: record.createdAt }; } offset += batchSize; hasMore = records.length === batchSize; } } } }; // lib/adapters/mail/NodemailerProvider.ts var import_nodemailer = __toESM(require("nodemailer")); var NodemailerProvider = class { /** * Create a new NodemailerProvider instance. * @param config - SMTP configuration * @throws {Error} If nodemailer is not installed */ constructor(config) { this.config = config; try { if (!import_nodemailer.default || !import_nodemailer.default.createTransport) { throw new Error("nodemailer is not properly initialized"); } } catch (error) { throw new Error( "NodemailerProvider requires nodemailer to be installed. Install it with: npm install nodemailer" ); } this.fromEmail = config.from || config.user; this.fromName = config.name || "Waitlist Manager"; this.transporter = import_nodemailer.default.createTransport({ host: config.host, port: config.port, secure: config.secure ?? config.port === 465, auth: { user: config.user, pass: config.pass } }); } /** * Verify the transporter configuration. * @returns true if configuration is valid */ async verify() { try { await this.transporter.verify(); return true; } catch (error) { console.error("Nodemailer verification failed:", error); return false; } } /** * Send a confirmation email. * @param email - Recipient email address * @param context - Template context with variables * @returns true if the email was sent successfully */ async sendConfirmation(email, context) { try { const subject = this.buildSubject(context); const html = this.buildHtml(context); const mailOptions = { from: `"${this.fromName}" <${this.fromEmail}>`, to: email, subject, html }; await this.transporter.sendMail(mailOptions); return true; } catch (error) { console.error(`Failed to send email to ${email}:`, error); return false; } } /** * Build the email subject. * Allows customization through context. */ buildSubject(context) { return context.subject || `Welcome to ${context.companyName || "our platform"}`; } /** * Build the HTML body. * Provides a default template if customHtml is not provided. */ buildHtml(context) { if (context.customHtml) { return context.customHtml; } return ` <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <style> body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; } .container { max-width: 600px; margin: 0 auto; padding: 20px; } .header { background-color: #007bff; color: white; padding: 20px; text-align: center; } .content { padding: 20px; } .footer { text-align: center; padding: 20px; color: #666; font-size: 12px; } </style> </head> <body> <div class="container"> <div class="header"> <h1>Welcome to ${context.companyName || "Our Platform"}</h1> </div> <div class="content"> <p>Hello,</p> <p>Thank you for joining our waitlist! We're excited to have you on board.</p> <p>You'll be among the first to know when we launch.</p> ${context.customUrl ? `<p><a href="${context.customUrl}" style="background-color: #007bff; color: white; padding: 10px 20px; text-decoration: none; border-radius: 4px; display: inline-block;">Learn More</a></p>` : ""} </div> <div class="footer"> <p>&copy; 2024 ${context.companyName || "Our Platform"}. All rights reserved.</p> </div> </div> </body> </html> `; } }; // lib/adapters/mail/ConsoleMailProvider.ts var ConsoleMailProvider = class { constructor(verbose = true) { this.verbose = verbose; } /** * Output a confirmation email to console. * @param email - Recipient email address * @param context - Email context * @returns Always returns true (successful "send") */ async sendConfirmation(email, context) { const timestamp = (/* @__PURE__ */ new Date()).toISOString(); const subject = this.buildSubject(context); const body = this.buildBody(context); console.log(` ${"=".repeat(60)}`); console.log(`[EMAIL SENT] ${timestamp}`); console.log(`To: ${email}`); console.log(`Subject: ${subject}`); if (this.verbose) { console.log(` Context:`); console.log(JSON.stringify(context, null, 2)); console.log(` Body:`); console.log(body); } console.log(`${"=".repeat(60)} `); return true; } buildSubject(context) { return context.subject || `Welcome to ${context.companyName || "our platform"}`; } buildBody(context) { return context.customHtml || ` Thank you for joining ${context.companyName || "our platform"}! Email: ${context.email} ${context.customUrl ? `Link: ${context.customUrl}` : ""} `.trim(); } }; // lib/index.ts var VERSION = "2.1.0"; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { ConsoleMailProvider, MemoryStorage, MongooseStorage, NodemailerProvider, SequelizeStorage, VERSION, WaitlistManager });