{"version":3,"file":"index.mjs","names":["log","log"],"sources":["../src/mailer.service.ts","../src/adapter.ts","../src/providers/smtp.provider.ts","../src/providers/console.provider.ts"],"sourcesContent":["import { Logger, createToken } from '@forinda/kickjs'\nimport type {\n  MailProvider,\n  MailMessage,\n  MailResult,\n  MailRecipient,\n  MailTemplateEngine,\n  MailerOptions,\n} from './types'\n\nconst log = Logger.for('Mailer')\n\n/** DI token for resolving MailerService from the container. */\nexport const MAILER = createToken<MailerService>('kick/mailer/Service')\n\n/**\n * Central mail service — send emails through any provider.\n *\n * @example\n * ```ts\n * @Service()\n * class UserService {\n *   constructor(@Inject(MAILER) private mailer: MailerService) {}\n *\n *   async sendWelcome(user: User) {\n *     await this.mailer.send({\n *       to: user.email,\n *       subject: 'Welcome!',\n *       html: '<h1>Welcome to our app</h1>',\n *     })\n *   }\n *\n *   // Or with templates:\n *   async sendInvoice(user: User, invoice: Invoice) {\n *     await this.mailer.sendTemplate('invoice', {\n *       to: user.email,\n *       subject: `Invoice #${invoice.number}`,\n *     }, { user, invoice })\n *   }\n * }\n * ```\n */\nexport class MailerService {\n  private readonly provider: MailProvider\n  private readonly defaultFrom?: MailRecipient\n  private readonly templateEngine?: MailTemplateEngine\n  private readonly enabled: boolean\n\n  // Counters surfaced via the MailerAdapter's introspect() to DevTools\n  // (architecture.md §23). Public + readonly externally so the adapter\n  // wrapper reads them without coupling to internal field layout.\n  /** Total messages successfully accepted by the provider. */\n  public sentCount = 0\n  /** Total messages that threw inside provider.send(). */\n  public failedCount = 0\n  /** Total messages skipped because `enabled = false` (dry-run mode). */\n  public dryRunCount = 0\n\n  constructor(options: MailerOptions) {\n    this.provider = options.provider\n    this.defaultFrom = options.defaultFrom\n    this.templateEngine = options.templateEngine\n    this.enabled = options.enabled ?? true\n  }\n\n  /**\n   * Send an email message.\n   * Applies defaultFrom if no from address is set.\n   */\n  async send(message: MailMessage): Promise<MailResult> {\n    const msg = { ...message }\n\n    // Apply default from\n    if (!msg.from && this.defaultFrom) {\n      msg.from = this.defaultFrom\n    }\n\n    if (!this.enabled) {\n      this.dryRunCount++\n      log.info(`[dry-run] → ${formatRecipient(msg.to)} | ${msg.subject}`)\n      return { messageId: 'dry-run', accepted: true }\n    }\n\n    try {\n      const result = await this.provider.send(msg)\n      this.sentCount++\n      log.info(`Sent → ${formatRecipient(msg.to)} | ${msg.subject} [${result.messageId}]`)\n      return result\n    } catch (err: any) {\n      this.failedCount++\n      log.error({ err }, `Failed → ${formatRecipient(msg.to)} | ${msg.subject}`)\n      throw err\n    }\n  }\n\n  /**\n   * Render a template and send the resulting HTML as an email.\n   * Requires a templateEngine to be configured.\n   *\n   * @param template - Template name (resolved by the engine)\n   * @param message - Mail message (html will be overwritten by the rendered template)\n   * @param data - Template variables\n   */\n  async sendTemplate(\n    template: string,\n    message: Omit<MailMessage, 'html'>,\n    data: Record<string, any>,\n  ): Promise<MailResult> {\n    if (!this.templateEngine) {\n      throw new Error(\n        'MailerService: templateEngine is required for sendTemplate(). ' +\n          'Pass one in MailerOptions or use send() with raw HTML.',\n      )\n    }\n\n    const html = await this.templateEngine.render(template, data)\n    return this.send({ ...message, html })\n  }\n\n  /** Get the underlying provider (for advanced use) */\n  getProvider(): MailProvider {\n    return this.provider\n  }\n\n  /** Shutdown the provider */\n  async shutdown(): Promise<void> {\n    if (this.provider.shutdown) {\n      await this.provider.shutdown()\n    }\n  }\n}\n\nfunction formatRecipient(to: MailRecipient | MailRecipient[]): string {\n  if (Array.isArray(to)) {\n    return to.map((r) => (typeof r === 'string' ? r : r.address)).join(', ')\n  }\n  return typeof to === 'string' ? to : to.address\n}\n","import { Logger, defineAdapter } from '@forinda/kickjs'\nimport { PROTOCOL_VERSION, type IntrospectionSnapshot } from '@forinda/kickjs-devtools-kit'\nimport { MailerService, MAILER } from './mailer.service'\nimport type { MailerOptions } from './types'\n\nconst log = Logger.for('MailerAdapter')\n\n/**\n * Mailer adapter — registers MailerService in the DI container.\n *\n * @example\n * ```ts\n * import { MailerAdapter, SmtpProvider } from '@forinda/kickjs-mailer'\n *\n * bootstrap({\n *   adapters: [\n *     MailerAdapter({\n *       provider: new SmtpProvider({ host: 'smtp.gmail.com', port: 587, auth: { ... } }),\n *       defaultFrom: { name: 'My App', address: 'noreply@myapp.com' },\n *     }),\n *   ],\n * })\n *\n * // Multiple providers via .scoped() — e.g. transactional + marketing pipelines:\n * bootstrap({\n *   adapters: [\n *     MailerAdapter.scoped('transactional', { provider: new ResendProvider({ ... }) }),\n *     MailerAdapter.scoped('marketing', { provider: new SesProvider({ ... }) }),\n *   ],\n * })\n * ```\n */\nexport const MailerAdapter = defineAdapter<MailerOptions>({\n  name: 'MailerAdapter',\n  build: (options) => {\n    const mailer = new MailerService(options)\n\n    return {\n      // ── DevTools introspection (architecture.md §23) ───────────────\n      introspect(): IntrospectionSnapshot {\n        return {\n          protocolVersion: PROTOCOL_VERSION,\n          name: 'MailerAdapter',\n          kind: 'adapter',\n          state: {\n            provider: options.provider.name,\n            enabled: options.enabled !== false,\n            hasTemplateEngine: !!options.templateEngine,\n          },\n          tokens: { provides: ['kick/mailer/Service'], requires: [] },\n          metrics: {\n            sent: mailer.sentCount,\n            failed: mailer.failedCount,\n            dryRun: mailer.dryRunCount,\n          },\n        }\n      },\n\n      beforeStart({ container }) {\n        container.registerInstance(MAILER, mailer)\n        log.info(\n          `Mail provider: ${options.provider.name}${options.enabled === false ? ' (disabled)' : ''}`,\n        )\n      },\n\n      async shutdown() {\n        await mailer.shutdown()\n        log.info('Mailer shut down')\n      },\n    }\n  },\n})\n","import type { MailProvider, MailMessage, MailResult } from '../types'\n\nexport interface SmtpOptions {\n  /** SMTP host (e.g. 'smtp.gmail.com', 'smtp.resend.com') */\n  host: string\n  /** SMTP port (default: 587) */\n  port?: number\n  /** Use TLS (default: true for port 465, false otherwise) */\n  secure?: boolean\n  /** Authentication credentials */\n  auth?: {\n    user: string\n    pass: string\n  }\n  /** Connection timeout in ms (default: 10000) */\n  connectionTimeout?: number\n}\n\n/**\n * SMTP mail provider using nodemailer.\n *\n * Requires `nodemailer` as a peer dependency:\n * ```bash\n * pnpm add nodemailer @types/nodemailer\n * ```\n *\n * @example\n * ```ts\n * // Gmail\n * new SmtpProvider({\n *   host: 'smtp.gmail.com',\n *   port: 587,\n *   auth: { user: 'you@gmail.com', pass: 'app-password' },\n * })\n *\n * // Resend via SMTP\n * new SmtpProvider({\n *   host: 'smtp.resend.com',\n *   port: 465,\n *   secure: true,\n *   auth: { user: 'resend', pass: getEnv('RESEND_API_KEY') },\n * })\n *\n * // Mailpit (local dev)\n * new SmtpProvider({ host: 'localhost', port: 1025 })\n * ```\n */\nexport class SmtpProvider implements MailProvider {\n  name = 'smtp'\n  private transporter: any\n\n  constructor(private options: SmtpOptions) {}\n\n  private async ensureTransporter(): Promise<void> {\n    if (this.transporter) return\n    try {\n      const nodemailer: any = await import('nodemailer')\n      const createTransport = nodemailer.createTransport ?? nodemailer.default?.createTransport\n      this.transporter = createTransport({\n        host: this.options.host,\n        port: this.options.port ?? 587,\n        secure: this.options.secure ?? this.options.port === 465,\n        auth: this.options.auth,\n        connectionTimeout: this.options.connectionTimeout ?? 10000,\n      })\n    } catch {\n      throw new Error('SmtpProvider requires \"nodemailer\" package. Install: pnpm add nodemailer')\n    }\n  }\n\n  async send(message: MailMessage): Promise<MailResult> {\n    await this.ensureTransporter()\n\n    const result = await this.transporter.sendMail({\n      from: formatAddress(message.from),\n      to: formatRecipients(message.to),\n      cc: message.cc ? formatRecipients(message.cc) : undefined,\n      bcc: message.bcc ? formatRecipients(message.bcc) : undefined,\n      replyTo: message.replyTo ? formatAddress(message.replyTo) : undefined,\n      subject: message.subject,\n      text: message.text,\n      html: message.html,\n      attachments: message.attachments,\n      headers: message.headers,\n    })\n\n    return {\n      messageId: result.messageId,\n      accepted: (result.accepted?.length ?? 0) > 0,\n      raw: result,\n    }\n  }\n\n  async shutdown(): Promise<void> {\n    if (this.transporter) {\n      this.transporter.close()\n    }\n  }\n}\n\nfunction formatAddress(addr: any): string | undefined {\n  if (!addr) return undefined\n  if (typeof addr === 'string') return addr\n  return addr.name ? `\"${addr.name}\" <${addr.address}>` : addr.address\n}\n\nfunction formatRecipients(recipients: any): string {\n  if (!recipients) return ''\n  if (typeof recipients === 'string') return recipients\n  if (Array.isArray(recipients)) {\n    return recipients.map((r: any) => formatAddress(r)).join(', ')\n  }\n  return formatAddress(recipients) ?? ''\n}\n","import { Logger, ref } from '@forinda/kickjs'\nimport type { MailProvider, MailMessage, MailResult } from '../types'\n\nconst log = Logger.for('ConsoleMail')\n\nlet counter = ref(0)\n\n/**\n * Console mail provider — logs emails instead of sending them.\n * Perfect for development and testing.\n *\n * @example\n * ```ts\n * MailerAdapter({\n *   provider: new ConsoleProvider(),\n *   defaultFrom: 'dev@localhost',\n * })\n * ```\n */\nexport class ConsoleProvider implements MailProvider {\n  name = 'console'\n\n  async send(message: MailMessage): Promise<MailResult> {\n    const id = `console-${++counter.value}`\n    const to = Array.isArray(message.to)\n      ? message.to.map((r) => (typeof r === 'string' ? r : r.address)).join(', ')\n      : typeof message.to === 'string'\n        ? message.to\n        : message.to.address\n\n    log.info(`────────────────────────────────────────`)\n    log.info(`From:    ${formatAddr(message.from)}`)\n    log.info(`To:      ${to}`)\n    if (message.cc) log.info(`CC:      ${formatAddr(message.cc)}`)\n    if (message.bcc) log.info(`BCC:     ${formatAddr(message.bcc)}`)\n    log.info(`Subject: ${message.subject}`)\n    if (message.text)\n      log.info(`Text:    ${message.text.slice(0, 200)}${message.text.length > 200 ? '...' : ''}`)\n    if (message.html)\n      log.info(`HTML:    ${message.html.slice(0, 200)}${message.html.length > 200 ? '...' : ''}`)\n    if (message.attachments?.length) {\n      log.info(`Attach:  ${message.attachments.map((a) => a.filename).join(', ')}`)\n    }\n    log.info(`ID:      ${id}`)\n    log.info(`────────────────────────────────────────`)\n\n    return { messageId: id, accepted: true }\n  }\n}\n\nfunction formatAddr(addr: any): string {\n  if (!addr) return '(none)'\n  if (typeof addr === 'string') return addr\n  if (Array.isArray(addr)) return addr.map(formatAddr).join(', ')\n  return addr.name ? `\"${addr.name}\" <${addr.address}>` : addr.address\n}\n"],"mappings":"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"}