@objectstack/plugin-email 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +22 -0
- package/CHANGELOG.md +11 -0
- package/LICENSE +202 -0
- package/dist/index.d.mts +256 -0
- package/dist/index.d.ts +256 -0
- package/dist/index.js +877 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +835 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +35 -0
- package/src/email-plugin.ts +361 -0
- package/src/email-service.test.ts +142 -0
- package/src/email-service.ts +366 -0
- package/src/index.ts +32 -0
- package/src/send-template.test.ts +273 -0
- package/src/template-engine.test.ts +76 -0
- package/src/template-engine.ts +94 -0
- package/src/templates/auth-templates.ts +177 -0
- package/src/transports/index.ts +39 -0
- package/src/transports/postmark.ts +94 -0
- package/src/transports/resend.ts +89 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/email-plugin.ts","../src/template-engine.ts","../src/email-service.ts","../src/transports/resend.ts","../src/transports/postmark.ts","../src/transports/index.ts","../src/templates/auth-templates.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport type { IDataEngine } from '@objectstack/spec/contracts';\nimport type {\n IEmailTransport,\n EmailAddress,\n} from '@objectstack/spec/contracts';\nimport { SysEmail, SysEmailTemplate } from '@objectstack/platform-objects/audit';\nimport { EmailService, LogTransport, type EmailPersistence, type TemplateLoader, type EmailTemplateRow } from './email-service.js';\nimport { makeTransport } from './transports/index.js';\nimport { BUILTIN_AUTH_TEMPLATES } from './templates/auth-templates.js';\nimport type { EmailTemplateDefinition as EmailTemplate } from '@objectstack/spec/system';\n\nconst SYSTEM_CTX = { isSystem: true, roles: [], permissions: [] } as const;\n\n/**\n * Plugin configuration.\n */\nexport interface EmailServicePluginOptions {\n /**\n * Pluggable delivery transport. When omitted the plugin builds one\n * from `provider`/`apiKey`; if both omitted, falls back to\n * `LogTransport` (no real send).\n */\n transport?: IEmailTransport;\n /** Provider tag — `'log' | 'resend' | 'postmark'`. Default `'log'`. */\n provider?: 'log' | 'resend' | 'postmark';\n /** API key for resend/postmark. */\n apiKey?: string;\n /** Provider-specific extra options (e.g. Postmark messageStream). */\n providerOptions?: Record<string, unknown>;\n /** Default `From` address applied when `input.from` is omitted. */\n defaultFrom?: EmailAddress;\n /** Persist each attempt to sys_email. Default true when ObjectQL engine present. */\n persist?: boolean;\n /** Retry attempts on transport throw. Default 0. */\n retries?: number;\n /** Default template render context (merged into every sendTemplate call). */\n defaultTemplateContext?: Record<string, unknown>;\n /** Seed built-in auth templates into sys_email_template on startup. Default true. */\n seedTemplates?: boolean;\n /** Additional templates seeded alongside the built-ins. */\n templates?: EmailTemplate[];\n}\n\n/**\n * EmailServicePlugin — registers the `email` service.\n *\n * Lifecycle:\n * - `init`: register sys_email + sys_email_template via manifest;\n * build transport (config → provider+apiKey → LogTransport fallback);\n * register a transport-only EmailService so dependents can resolve it.\n * - `start` (kernel:ready): wire ObjectQL-backed sys_email persistence\n * + sys_email_template TemplateLoader; seed built-in auth templates\n * (upsert by `(name, locale)`).\n */\nexport class EmailServicePlugin implements Plugin {\n name = 'com.objectstack.service.email';\n version = '1.0.0';\n type = 'standard';\n dependencies = ['com.objectstack.engine.objectql'];\n\n private readonly options: EmailServicePluginOptions;\n private service?: EmailService;\n\n constructor(options: EmailServicePluginOptions = {}) {\n this.options = options;\n }\n\n private resolveTransport(ctx: PluginContext): IEmailTransport {\n if (this.options.transport) return this.options.transport;\n const provider = this.options.provider ?? 'log';\n if (provider === 'log') return new LogTransport(ctx.logger);\n return makeTransport({\n provider,\n apiKey: this.options.apiKey,\n options: this.options.providerOptions,\n logger: ctx.logger,\n });\n }\n\n async init(ctx: PluginContext): Promise<void> {\n // Register sys_email + sys_email_template via manifest service.\n ctx.getService<{ register(m: any): void }>('manifest').register({\n id: 'com.objectstack.service.email',\n name: 'Email Service',\n version: '1.0.0',\n type: 'plugin',\n scope: 'system',\n defaultDatasource: 'cloud',\n namespace: 'sys',\n objects: [SysEmail, SysEmailTemplate],\n });\n\n const transport = this.resolveTransport(ctx);\n if (!this.options.transport && (this.options.provider ?? 'log') === 'log') {\n ctx.logger.info(\n 'EmailServicePlugin: no transport configured — using LogTransport (mail will NOT be sent)',\n );\n } else {\n ctx.logger.info(\n `EmailServicePlugin: using '${this.options.provider ?? 'log'}' provider`,\n );\n }\n\n // Persistence + templateLoader are wired in `start` once the\n // ObjectQL engine is available; here we register the service\n // synchronously so dependents can resolve it.\n this.service = new EmailService({\n transport,\n defaultFrom: this.options.defaultFrom,\n retries: this.options.retries,\n defaultTemplateContext: this.options.defaultTemplateContext,\n logger: ctx.logger,\n });\n ctx.registerService('email', this.service);\n ctx.logger.info('EmailServicePlugin: email service registered');\n }\n\n async start(ctx: PluginContext): Promise<void> {\n ctx.hook('kernel:ready', async () => {\n let engine: IDataEngine | null = null;\n try { engine = ctx.getService<IDataEngine>('objectql'); }\n catch { try { engine = ctx.getService<IDataEngine>('data'); } catch { /* ignore */ } }\n if (!engine || !this.service) return;\n\n // ── Bind to the `mail` settings namespace (Phase 1) ──────────────\n // Allows the admin UI to live-update SMTP/provider/from-address\n // without restarting the process. Env-locked fields still win at\n // the resolver level, so config-via-env keeps its precedence.\n try {\n const settings = ctx.getService<any>('settings');\n if (settings && typeof settings.createClient === 'function') {\n const applySettings = async () => {\n try {\n const payload = await settings.getNamespace('mail');\n const values: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(payload.values as Record<string, any>)) {\n values[k] = v?.value;\n }\n this.applyMailSettings(values, ctx);\n } catch (err: any) {\n ctx.logger.warn('EmailServicePlugin: failed to apply mail settings: ' + (err?.message ?? err));\n }\n };\n await applySettings();\n // Subscribe to namespace changes; rebuild on every update.\n if (typeof settings.subscribe === 'function') {\n settings.subscribe('mail', () => {\n void applySettings();\n });\n ctx.logger.info('EmailServicePlugin: bound to settings:changed for namespace=mail');\n }\n\n // Register the `mail/test` action handler so saving + sending\n // a test email actually exercises the live transport.\n if (typeof settings.registerAction === 'function') {\n const svc = this.service;\n settings.registerAction('mail', 'test', async ({ values, ctx: actionCtx }: any) => {\n const to = (actionCtx?.body?.to as string | undefined)\n ?? (values.from_email as string | undefined);\n if (!to) {\n return { ok: false, severity: 'error', message: 'Provide a \"to\" address (or set from_email).' };\n }\n try {\n const result = await svc.send({\n to,\n from: values.from_email ? {\n address: String(values.from_email),\n name: values.from_name ? String(values.from_name) : undefined,\n } : undefined,\n subject: 'ObjectStack mail test',\n text: 'This is a test email from the ObjectStack settings page.',\n });\n if (result.status === 'failed') {\n return { ok: false, severity: 'error', message: result.error ?? 'Send failed.' };\n }\n return {\n ok: true,\n severity: 'info',\n message: `Sent test email to ${to} (id=${result.id}).`,\n };\n } catch (err: any) {\n return { ok: false, severity: 'error', message: err?.message ?? String(err) };\n }\n });\n }\n }\n } catch {\n // settings service not registered — env/constructor opts remain authoritative.\n }\n\n const persistence: EmailPersistence | undefined = this.options.persist === false\n ? undefined\n : {\n async insert(row) {\n const created = await (engine as any).insert('sys_email', row, {\n context: SYSTEM_CTX,\n });\n return created?.id ? { id: String(created.id) } : { id: String(row.id) };\n },\n async update(id, patch) {\n await (engine as any).update('sys_email', { id, ...patch }, {\n context: SYSTEM_CTX,\n });\n },\n };\n\n const templateLoader: TemplateLoader = {\n async load(name, locale) {\n const where: Record<string, unknown> = { name };\n if (locale) where.locale = locale;\n const rows = await (engine as any).find('sys_email_template', {\n where,\n limit: 1,\n context: SYSTEM_CTX,\n });\n const row = Array.isArray(rows) ? rows[0] : (rows as any)?.data?.[0];\n return (row as EmailTemplateRow) || null;\n },\n };\n\n // Mutate the existing service instance so consumers that already\n // captured a reference (e.g. AuthManager) see the upgrade.\n if (persistence) this.service.setPersistence(persistence);\n this.service.setTemplateLoader(templateLoader);\n ctx.logger.info('EmailServicePlugin: sys_email persistence + template loader enabled');\n\n // Bind 'email.send.async' queue subscriber for durable, retry-on-failure delivery.\n // Producers: `queue.publish('email.send.async', sendInput, { maxAttempts: 5, backoff: {...} })`\n // The queue handles retry / DLQ via sys_job_queue.\n try {\n const queue: any = ctx.getService<any>('queue');\n if (queue && typeof queue.subscribe === 'function' && this.service) {\n const svc = this.service;\n await queue.subscribe('email.send.async', async (msg: any) => {\n const result = await svc.send(msg.data);\n if (result.status === 'failed') {\n // Force the queue to retry / DLQ by throwing\n throw new Error(result.error ?? 'email send failed');\n }\n });\n ctx.logger.info('EmailServicePlugin: subscribed to email.send.async queue');\n }\n } catch (err) {\n ctx.logger.warn('EmailServicePlugin: email.send.async subscription failed', err as any);\n }\n\n // Seed built-in + user-provided templates (upsert by name+locale).\n if (this.options.seedTemplates !== false) {\n const all = [\n ...BUILTIN_AUTH_TEMPLATES,\n ...(this.options.templates ?? []),\n ];\n for (const tpl of all) {\n try { await this.upsertTemplate(engine!, tpl); }\n catch (err: any) {\n ctx.logger.warn(`EmailServicePlugin: seed template failed: ${tpl.name} ${tpl.locale}`, err?.message || err);\n }\n }\n ctx.logger.info(`EmailServicePlugin: seeded ${all.length} template row(s)`);\n }\n });\n }\n\n /**\n * Translate the `mail` settings namespace snapshot into a transport\n * and `defaultFrom`, then hot-swap them on the running EmailService.\n *\n * Behaviour:\n * - `provider = 'log' | 'smtp'` keeps the LogTransport (real SMTP\n * delivery requires `@objectstack/plugin-mail-smtp`, which is not\n * a dependency of this package). The from-address is still applied.\n * - `provider = 'resend' | 'postmark'` rebuilds the transport using\n * `api_key` from settings. If `api_key` is missing the swap is\n * skipped and a warning is logged — the previous transport stays.\n *\n * Env-locked fields (handled in SettingsService.get) still resolve\n * before this method ever sees them, so an env override transparently\n * wins.\n */\n private applyMailSettings(values: Record<string, unknown>, ctx: PluginContext): void {\n if (!this.service) return;\n\n const fromEmail = typeof values.from_email === 'string' ? values.from_email : undefined;\n const fromName = typeof values.from_name === 'string' ? values.from_name : undefined;\n if (fromEmail) this.service.setDefaultFrom({ address: fromEmail, name: fromName });\n\n const provider = String(values.provider ?? 'smtp');\n if (provider === 'smtp' || provider === 'log') {\n // No SMTP transport ships in core; settings-only edits become\n // a no-op for transport but still apply `defaultFrom`. Users\n // wanting real SMTP install `@objectstack/plugin-mail-smtp`\n // and configure it via constructor opts.\n ctx.logger.info(\n `EmailServicePlugin: mail settings applied (provider=${provider}, from=${fromEmail ?? '∅'}); transport unchanged.`,\n );\n return;\n }\n\n const apiKey = typeof values.api_key === 'string' ? values.api_key : undefined;\n if (!apiKey) {\n ctx.logger.warn(\n `EmailServicePlugin: provider='${provider}' selected but api_key is empty — transport NOT rebuilt.`,\n );\n return;\n }\n\n try {\n const transport = makeTransport({\n provider: provider as 'resend' | 'postmark',\n apiKey,\n logger: ctx.logger,\n });\n this.service.setTransport(transport);\n ctx.logger.info(`EmailServicePlugin: transport rebuilt from settings (provider=${provider}).`);\n } catch (err: any) {\n ctx.logger.warn('EmailServicePlugin: failed to rebuild transport: ' + (err?.message ?? err));\n }\n }\n\n private async upsertTemplate(engine: IDataEngine, tpl: EmailTemplate): Promise<void> {\n const row = {\n name: tpl.name,\n label: tpl.label,\n category: tpl.category,\n locale: tpl.locale,\n subject: tpl.subject,\n body_html: tpl.bodyHtml,\n ...(tpl.bodyText ? { body_text: tpl.bodyText } : {}),\n ...(tpl.fromOverride?.address ? {\n from_address: tpl.fromOverride.address,\n ...(tpl.fromOverride.name ? { from_name: tpl.fromOverride.name } : {}),\n } : {}),\n ...(tpl.replyTo ? { reply_to: tpl.replyTo } : {}),\n active: tpl.active,\n is_system: tpl.isSystem,\n ...(tpl.description ? { description: tpl.description } : {}),\n ...(tpl.variables?.length ? { variables_json: JSON.stringify(tpl.variables) } : {}),\n };\n const existing = await (engine as any).find('sys_email_template', {\n where: { name: tpl.name, locale: tpl.locale },\n limit: 1,\n context: SYSTEM_CTX,\n });\n const existingRow = Array.isArray(existing) ? existing[0] : (existing as any)?.data?.[0];\n if (existingRow?.id) {\n // Only re-seed if the existing row is system-managed (is_system=true);\n // never overwrite a tenant-customised row.\n if (existingRow.is_system === false) return;\n await (engine as any).update('sys_email_template', { id: existingRow.id, ...row }, {\n context: SYSTEM_CTX,\n });\n } else {\n await (engine as any).insert('sys_email_template', row, {\n context: SYSTEM_CTX,\n });\n }\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Minimal mustache-style template renderer.\n *\n * Supports `{{path.to.value}}` placeholders resolved against a plain\n * JS object via dotted-path lookup. Values are HTML-escaped by\n * default; use `{{{path}}}` (triple braces) to opt out of escaping\n * (e.g. when injecting pre-rendered HTML fragments such as URLs in\n * `<a href=\"\">`).\n *\n * Deliberately tiny (no loops / conditionals / partials) — the design\n * stance is that email templates SHOULD be data-only renderings; any\n * branching belongs in the caller. If we ever need more, swap for\n * Handlebars, but bringing it in costs ~50KB and pulls a parser at\n * runtime; we resist that until a real use case demands it.\n */\n\nconst PLACEHOLDER = /(\\{\\{\\{?)\\s*([\\w.]+)\\s*(\\}?\\}\\})/g;\n\nfunction lookup(data: Record<string, any>, path: string): unknown {\n if (!path) return undefined;\n const parts = path.split('.');\n let cur: any = data;\n for (const p of parts) {\n if (cur == null) return undefined;\n cur = cur[p];\n }\n return cur;\n}\n\nfunction escapeHtml(s: string): string {\n return s\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\n/**\n * Render `template` with values from `data`. Missing placeholders\n * render as empty strings (no throw); call `requireVars()` first if\n * you need strict validation.\n */\nexport function renderTemplate(template: string, data: Record<string, any>): string {\n if (!template) return '';\n return template.replace(PLACEHOLDER, (_match, open: string, path: string, close: string) => {\n const isUnescaped = open === '{{{' && close === '}}}';\n const raw = lookup(data, path);\n if (raw == null) return '';\n const str = typeof raw === 'string' ? raw : String(raw);\n return isUnescaped ? str : escapeHtml(str);\n });\n}\n\n/**\n * Throw `Error('MISSING_VARIABLES: a, b')` when required vars are\n * absent from `data`. Used by `IEmailService.sendTemplate()` to\n * fail fast rather than send a half-rendered email.\n */\nexport function requireVars(\n data: Record<string, any>,\n required: ReadonlyArray<string>,\n): void {\n const missing = required.filter((name) => lookup(data, name) == null);\n if (missing.length > 0) {\n throw new Error(`MISSING_VARIABLES: ${missing.join(', ')}`);\n }\n}\n\n/**\n * Strip HTML tags + collapse whitespace to derive a plain-text body\n * from an HTML template. Conservative: keeps line breaks at block\n * boundaries (<br>, </p>, </div>) so the resulting text is at least\n * paragraph-shaped.\n */\nexport function htmlToText(html: string): string {\n if (!html) return '';\n return html\n .replace(/<br\\s*\\/?>/gi, '\\n')\n .replace(/<\\/(p|div|h[1-6]|li|tr)>/gi, '\\n')\n .replace(/<[^>]+>/g, '')\n .replace(/ /g, ' ')\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .replace(/[ \\t]+/g, ' ')\n .replace(/\\n[ \\t]+/g, '\\n')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim();\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type {\n IEmailService,\n IEmailTransport,\n SendEmailInput,\n SendEmailResult,\n SendTemplateInput,\n NormalizedEmailMessage,\n EmailAddress,\n EmailDeliveryStatus,\n TransportSendResult,\n} from '@objectstack/spec/contracts';\nimport { renderTemplate, requireVars, htmlToText } from './template-engine.js';\n\n/**\n * Internal persistence shim — typed loosely so the service can run\n * without an ObjectQL engine wired (e.g. unit tests, serverless).\n */\nexport interface EmailPersistence {\n insert(row: Record<string, any>): Promise<{ id: string } | string>;\n update?(id: string, patch: Record<string, any>): Promise<void>;\n}\n\n/**\n * Naive RFC-5322 validator — good enough to catch obvious typos.\n * Defers full validation to the transport / receiving MTA.\n */\nconst EMAIL_REGEX = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n\n/**\n * Format an EmailAddress (string or {name,address}) into the canonical\n * `\"Display\" <addr>` form. Throws if address is malformed.\n */\nexport function formatAddress(addr: EmailAddress): string {\n const obj = typeof addr === 'string' ? { address: addr } : addr;\n const address = String(obj.address ?? '').trim();\n if (!EMAIL_REGEX.test(address)) {\n throw new Error(`Invalid email address: ${address || '(empty)'}`);\n }\n const name = obj.name?.trim();\n if (!name) return address;\n // Quote display name if it contains characters that need quoting\n const needsQuote = /[\",()<>@:;.\\\\\\[\\]]/.test(name);\n const quoted = needsQuote ? `\"${name.replace(/\"/g, '\\\\\"')}\"` : name;\n return `${quoted} <${address}>`;\n}\n\nfunction listToArray(v: EmailAddress | EmailAddress[] | undefined): string[] | undefined {\n if (v === undefined) return undefined;\n const arr = Array.isArray(v) ? v : [v];\n return arr.map(formatAddress);\n}\n\n/**\n * Validate input + apply default-from + canonicalize recipients.\n * Throws Error('VALIDATION_FAILED: <reason>') for malformed payloads.\n */\nexport function normalizeMessage(\n input: SendEmailInput,\n defaultFrom?: EmailAddress,\n): NormalizedEmailMessage {\n if (!input || typeof input !== 'object') {\n throw new Error('VALIDATION_FAILED: input must be an object');\n }\n const subject = String(input.subject ?? '').trim();\n if (!subject) throw new Error('VALIDATION_FAILED: subject is required');\n if (!input.text && !input.html) {\n throw new Error('VALIDATION_FAILED: at least one of text or html is required');\n }\n const toArr = listToArray(input.to);\n if (!toArr || toArr.length === 0) {\n throw new Error('VALIDATION_FAILED: at least one recipient (to) is required');\n }\n const fromCandidate = input.from ?? defaultFrom;\n if (!fromCandidate) {\n throw new Error('VALIDATION_FAILED: from address required (set options.defaultFrom or pass input.from)');\n }\n const from = formatAddress(fromCandidate);\n\n const msg: NormalizedEmailMessage = {\n to: toArr,\n from,\n subject,\n ...(input.text !== undefined ? { text: input.text } : {}),\n ...(input.html !== undefined ? { html: input.html } : {}),\n };\n const cc = listToArray(input.cc);\n if (cc && cc.length > 0) msg.cc = cc;\n const bcc = listToArray(input.bcc);\n if (bcc && bcc.length > 0) msg.bcc = bcc;\n if (input.replyTo) msg.replyTo = formatAddress(input.replyTo);\n if (input.attachments && input.attachments.length > 0) msg.attachments = input.attachments;\n if (input.headers && Object.keys(input.headers).length > 0) msg.headers = input.headers;\n return msg;\n}\n\n/**\n * Development transport — never actually sends. Logs to the provided\n * logger and returns a synthetic Message-ID. Useful for local dev,\n * tests, and \"dry run\" environments.\n */\nexport class LogTransport implements IEmailTransport {\n private counter = 0;\n constructor(private readonly logger?: { info: (msg: string, meta?: any) => void }) {}\n async send(message: NormalizedEmailMessage): Promise<TransportSendResult> {\n const messageId = `<dev-${Date.now()}-${++this.counter}@objectstack.local>`;\n this.logger?.info('[LogTransport] would send email', {\n messageId,\n to: message.to,\n from: message.from,\n subject: message.subject,\n hasText: !!message.text,\n hasHtml: !!message.html,\n attachments: message.attachments?.length ?? 0,\n });\n return { messageId, response: 'logged' };\n }\n}\n\n/**\n * Generate a UUID-like id without pulling crypto in test contexts.\n * Uses crypto.randomUUID when available, falls back to a v4-shaped\n * random string. NOT cryptographically secure when the fallback is\n * used; the only consumer is local row identifiers, never tokens.\n */\nfunction newId(): string {\n try {\n const g = (globalThis as any).crypto;\n if (g?.randomUUID) return g.randomUUID();\n } catch { /* fall through */ }\n const hex = (n: number) => Math.floor(Math.random() * 16 ** n).toString(16).padStart(n, '0');\n return `${hex(8)}-${hex(4)}-4${hex(3)}-a${hex(3)}-${hex(12)}`;\n}\n\n/**\n * Loader for sys_email_template rows. Injected by EmailServicePlugin\n * on `kernel:ready`. Returns the best-matching row for `(name, locale)`\n * or `null` when none exists / inactive.\n */\nexport interface TemplateLoader {\n load(name: string, locale: string | undefined): Promise<EmailTemplateRow | null>;\n}\n\n/**\n * Row shape returned by the loader — mirrors sys_email_template\n * columns relevant to rendering.\n */\nexport interface EmailTemplateRow {\n name: string;\n locale: string;\n subject: string;\n body_html: string;\n body_text?: string | null;\n from_name?: string | null;\n from_address?: string | null;\n reply_to?: string | null;\n active?: boolean;\n variables_json?: string | null;\n}\n\nexport interface EmailServiceOptions {\n transport: IEmailTransport;\n defaultFrom?: EmailAddress;\n /** Persist each attempt to sys_email. Omit to disable persistence. */\n persistence?: EmailPersistence;\n /** Resolve named templates for sendTemplate(). Omit to disable templates. */\n templateLoader?: TemplateLoader;\n /** Retry attempts on transport throw. Default 0 (no retry). */\n retries?: number;\n /** Logger for diagnostic output. */\n logger?: { info: (msg: string, meta?: any) => void; warn: (msg: string, meta?: any) => void; error?: (msg: string, meta?: any) => void };\n /** Default render context merged into every sendTemplate call (e.g. `{ appName }`). */\n defaultTemplateContext?: Record<string, unknown>;\n}\n\n/**\n * Concrete IEmailService implementation.\n *\n * Flow:\n * 1. Validate + normalize input (throws on bad input).\n * 2. Persist queued row to sys_email (best-effort; failures logged).\n * 3. Call transport.send(); on success, update row to sent +\n * timestamp + messageId. On failure, mark failed + error.\n * 4. Return SendEmailResult with the persisted row id (or a fresh\n * id when persistence is disabled).\n */\nexport class EmailService implements IEmailService {\n constructor(public options: EmailServiceOptions) {\n if (!options.transport) throw new Error('EmailService: transport is required');\n }\n\n /** Wire (or replace) the template loader after construction. */\n setTemplateLoader(loader: TemplateLoader): void {\n this.options.templateLoader = loader;\n }\n\n /** Wire (or replace) persistence after construction. */\n setPersistence(persistence: EmailPersistence | undefined): void {\n this.options.persistence = persistence;\n }\n\n /**\n * Hot-swap the underlying transport. Used by EmailServicePlugin when\n * the `mail` settings namespace changes (e.g. SMTP host updated in\n * the admin UI) so subsequent `send()` calls go through the new\n * transport without restarting the process.\n */\n setTransport(transport: IEmailTransport): void {\n this.options.transport = transport;\n }\n\n /** Replace the default `from` address used when callers omit `input.from`. */\n setDefaultFrom(from: EmailAddress | undefined): void {\n this.options.defaultFrom = from;\n }\n\n async send(input: SendEmailInput): Promise<SendEmailResult> {\n let normalized: NormalizedEmailMessage;\n try {\n normalized = normalizeMessage(input, this.options.defaultFrom);\n } catch (err: any) {\n // Validation failures must surface to the caller.\n throw err;\n }\n\n const id = newId();\n const baseRow: Record<string, any> = {\n id,\n from_address: normalized.from,\n to_addresses: normalized.to.join(', '),\n ...(normalized.cc?.length ? { cc_addresses: normalized.cc.join(', ') } : {}),\n ...(normalized.bcc?.length ? { bcc_addresses: normalized.bcc.join(', ') } : {}),\n ...(normalized.replyTo ? { reply_to: normalized.replyTo } : {}),\n subject: normalized.subject,\n ...(normalized.text !== undefined ? { body_text: normalized.text } : {}),\n ...(normalized.html !== undefined ? { body_html: normalized.html } : {}),\n ...(input.relatedObject ? { related_object: input.relatedObject } : {}),\n ...(input.relatedId ? { related_id: input.relatedId } : {}),\n ...(input.sentBy ? { sent_by: input.sentBy } : {}),\n status: 'queued',\n attempt_count: 0,\n };\n\n let persistedId: string | undefined;\n if (this.options.persistence) {\n try {\n const res = await this.options.persistence.insert(baseRow);\n persistedId = typeof res === 'string' ? res : res?.id ?? id;\n } catch (err: any) {\n this.options.logger?.warn('EmailService: sys_email persist failed (non-fatal)', { error: err?.message });\n }\n }\n const rowId = persistedId ?? id;\n\n const maxAttempts = (this.options.retries ?? 0) + 1;\n let lastError: any;\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n try {\n const result = await this.options.transport.send(normalized);\n const messageId = result.messageId;\n const status: EmailDeliveryStatus = 'sent';\n await this.updateRow(rowId, {\n status,\n message_id: messageId,\n sent_at: new Date().toISOString(),\n attempt_count: attempt,\n });\n return { id: rowId, status, messageId };\n } catch (err: any) {\n lastError = err;\n if (attempt < maxAttempts) {\n // simple exponential backoff\n await new Promise(r => setTimeout(r, Math.min(2000, 100 * 2 ** (attempt - 1))));\n }\n }\n }\n const errMessage = String(lastError?.message ?? lastError ?? 'send failed').slice(0, 1000);\n await this.updateRow(rowId, {\n status: 'failed',\n error: errMessage,\n attempt_count: maxAttempts,\n });\n return { id: rowId, status: 'failed', error: errMessage };\n }\n\n private async updateRow(id: string, patch: Record<string, any>): Promise<void> {\n if (!this.options.persistence?.update) return;\n try {\n await this.options.persistence.update(id, patch);\n } catch (err: any) {\n this.options.logger?.warn('EmailService: sys_email update failed (non-fatal)', { id, error: err?.message });\n }\n }\n\n /**\n * Render a named template from sys_email_template and deliver via\n * send(). Looks up `(name, locale)` then falls back to `(name, 'en-US')`.\n */\n async sendTemplate(input: SendTemplateInput): Promise<SendEmailResult> {\n if (!input?.template) {\n throw new Error('VALIDATION_FAILED: template name is required');\n }\n const loader = this.options.templateLoader;\n if (!loader) {\n throw new Error('TEMPLATE_NOT_FOUND: no templateLoader configured on EmailService');\n }\n const preferred = input.locale && String(input.locale).trim();\n let row = await loader.load(input.template, preferred || undefined);\n if (!row && preferred && preferred !== 'en-US') {\n row = await loader.load(input.template, 'en-US');\n }\n if (!row) {\n throw new Error(`TEMPLATE_NOT_FOUND: ${input.template} (locale=${preferred || 'en-US'})`);\n }\n if (row.active === false) {\n throw new Error(`TEMPLATE_INACTIVE: ${input.template}`);\n }\n\n // Validate required variables (declared in variables_json).\n const data: Record<string, any> = {\n ...(this.options.defaultTemplateContext || {}),\n ...(input.data || {}),\n };\n if (row.variables_json) {\n try {\n const decl: Array<{ name: string; required?: boolean }> = JSON.parse(String(row.variables_json));\n const required = decl.filter((v) => v?.required).map((v) => v.name);\n if (required.length) requireVars(data, required);\n } catch (err: any) {\n if (String(err?.message).startsWith('MISSING_VARIABLES')) throw err;\n this.options.logger?.warn('EmailService: variables_json parse failed (ignored)', { template: input.template });\n }\n }\n\n const subject = renderTemplate(row.subject, data);\n const html = renderTemplate(row.body_html, data);\n const text = row.body_text\n ? renderTemplate(row.body_text, data)\n : htmlToText(html);\n\n const from: EmailAddress | undefined = input.from\n ?? (row.from_address\n ? { address: row.from_address, ...(row.from_name ? { name: row.from_name } : {}) }\n : undefined);\n\n const sendInput: SendEmailInput = {\n to: input.to,\n subject,\n html,\n text,\n ...(from ? { from } : {}),\n ...(input.cc ? { cc: input.cc } : {}),\n ...(input.bcc ? { bcc: input.bcc } : {}),\n ...(input.replyTo ?? row.reply_to\n ? { replyTo: input.replyTo ?? (row.reply_to as string) }\n : {}),\n ...(input.attachments ? { attachments: input.attachments } : {}),\n ...(input.headers ? { headers: input.headers } : {}),\n ...(input.relatedObject ? { relatedObject: input.relatedObject } : {}),\n ...(input.relatedId ? { relatedId: input.relatedId } : {}),\n ...(input.sentBy ? { sentBy: input.sentBy } : {}),\n };\n return this.send(sendInput);\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type {\n IEmailTransport,\n NormalizedEmailMessage,\n TransportSendResult,\n} from '@objectstack/spec/contracts';\n\n/**\n * ResendTransport — SaaS delivery via https://resend.com\n *\n * Implements `IEmailTransport` using the Resend HTTPS API. Zero\n * external dependencies (uses fetch). API docs:\n * https://resend.com/docs/api-reference/emails/send-email\n *\n * @example\n * ```ts\n * new EmailServicePlugin({\n * transport: new ResendTransport(process.env.RESEND_API_KEY!),\n * defaultFrom: { name: 'Acme', address: 'no-reply@acme.com' },\n * });\n * ```\n */\nexport interface ResendTransportOptions {\n apiKey: string;\n /** Override the API host (used by tests / proxies). */\n endpoint?: string;\n}\n\nexport class ResendTransport implements IEmailTransport {\n private readonly apiKey: string;\n private readonly endpoint: string;\n\n constructor(apiKeyOrOptions: string | ResendTransportOptions) {\n if (typeof apiKeyOrOptions === 'string') {\n this.apiKey = apiKeyOrOptions;\n this.endpoint = 'https://api.resend.com/emails';\n } else {\n this.apiKey = apiKeyOrOptions.apiKey;\n this.endpoint = apiKeyOrOptions.endpoint || 'https://api.resend.com/emails';\n }\n if (!this.apiKey) {\n throw new Error('ResendTransport: apiKey is required');\n }\n }\n\n async send(message: NormalizedEmailMessage): Promise<TransportSendResult> {\n const body: Record<string, unknown> = {\n from: message.from,\n to: message.to,\n subject: message.subject,\n };\n if (message.html !== undefined) body.html = message.html;\n if (message.text !== undefined) body.text = message.text;\n if (message.cc?.length) body.cc = message.cc;\n if (message.bcc?.length) body.bcc = message.bcc;\n if (message.replyTo) body.reply_to = message.replyTo;\n if (message.headers && Object.keys(message.headers).length > 0) body.headers = message.headers;\n if (message.attachments?.length) {\n body.attachments = message.attachments.map((a) => ({\n filename: a.filename,\n content: typeof a.content === 'string'\n ? a.content\n : (a.content as any)?.toString?.('base64') ?? String(a.content),\n ...(a.contentType ? { content_type: a.contentType } : {}),\n }));\n }\n\n const res = await fetch(this.endpoint, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(body),\n });\n\n if (!res.ok) {\n const errText = await res.text().catch(() => '');\n throw new Error(`Resend ${res.status}: ${errText.slice(0, 500)}`);\n }\n const json = await res.json().catch(() => ({} as any));\n const messageId = String((json as any)?.id ?? '');\n if (!messageId) {\n throw new Error('Resend: response missing `id` field');\n }\n return { messageId, response: 'resend:ok' };\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type {\n IEmailTransport,\n NormalizedEmailMessage,\n TransportSendResult,\n} from '@objectstack/spec/contracts';\n\n/**\n * PostmarkTransport — SaaS delivery via https://postmarkapp.com\n *\n * Implements `IEmailTransport` using the Postmark HTTPS API. Zero\n * external dependencies (uses fetch). API docs:\n * https://postmarkapp.com/developer/user-guide/send-email-with-api\n *\n * @example\n * ```ts\n * new EmailServicePlugin({\n * transport: new PostmarkTransport({\n * apiKey: process.env.POSTMARK_TOKEN!,\n * messageStream: 'outbound',\n * }),\n * defaultFrom: { name: 'Acme', address: 'no-reply@acme.com' },\n * });\n * ```\n */\nexport interface PostmarkTransportOptions {\n apiKey: string;\n /** Postmark message stream (default 'outbound'). */\n messageStream?: string;\n /** Override the API host. */\n endpoint?: string;\n}\n\nexport class PostmarkTransport implements IEmailTransport {\n private readonly apiKey: string;\n private readonly endpoint: string;\n private readonly messageStream: string;\n\n constructor(opts: PostmarkTransportOptions) {\n if (!opts?.apiKey) throw new Error('PostmarkTransport: apiKey is required');\n this.apiKey = opts.apiKey;\n this.endpoint = opts.endpoint || 'https://api.postmarkapp.com/email';\n this.messageStream = opts.messageStream || 'outbound';\n }\n\n async send(message: NormalizedEmailMessage): Promise<TransportSendResult> {\n const body: Record<string, unknown> = {\n From: message.from,\n To: message.to.join(', '),\n Subject: message.subject,\n MessageStream: this.messageStream,\n };\n if (message.html !== undefined) body.HtmlBody = message.html;\n if (message.text !== undefined) body.TextBody = message.text;\n if (message.cc?.length) body.Cc = message.cc.join(', ');\n if (message.bcc?.length) body.Bcc = message.bcc.join(', ');\n if (message.replyTo) body.ReplyTo = message.replyTo;\n if (message.headers && Object.keys(message.headers).length > 0) {\n body.Headers = Object.entries(message.headers).map(([Name, Value]) => ({ Name, Value }));\n }\n if (message.attachments?.length) {\n body.Attachments = message.attachments.map((a) => ({\n Name: a.filename,\n Content: typeof a.content === 'string'\n ? a.content\n : (a.content as any)?.toString?.('base64') ?? String(a.content),\n ContentType: a.contentType || 'application/octet-stream',\n ...(a.cid ? { ContentID: `cid:${a.cid}` } : {}),\n }));\n }\n\n const res = await fetch(this.endpoint, {\n method: 'POST',\n headers: {\n 'X-Postmark-Server-Token': this.apiKey,\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n },\n body: JSON.stringify(body),\n });\n\n if (!res.ok) {\n const errText = await res.text().catch(() => '');\n throw new Error(`Postmark ${res.status}: ${errText.slice(0, 500)}`);\n }\n const json: any = await res.json().catch(() => ({}));\n const messageId = String(json?.MessageID ?? '');\n if (!messageId) {\n throw new Error('Postmark: response missing `MessageID` field');\n }\n return { messageId, response: 'postmark:ok' };\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { IEmailTransport } from '@objectstack/spec/contracts';\nimport { LogTransport } from '../email-service.js';\nimport { ResendTransport } from './resend.js';\nimport { PostmarkTransport } from './postmark.js';\n\nexport { ResendTransport, type ResendTransportOptions } from './resend.js';\nexport { PostmarkTransport, type PostmarkTransportOptions } from './postmark.js';\n\nexport interface MakeTransportOptions {\n provider: 'log' | 'resend' | 'postmark';\n apiKey?: string;\n options?: Record<string, unknown>;\n logger?: { info: (msg: string, meta?: any) => void };\n}\n\n/**\n * Build an IEmailTransport from a provider tag + opts. Used by\n * EmailServicePlugin to materialise the transport selected by\n * `EmailServiceConfig.provider`.\n *\n * Throws when a non-`log` provider is requested without an `apiKey`.\n */\nexport function makeTransport(opts: MakeTransportOptions): IEmailTransport {\n const { provider, apiKey, options = {}, logger } = opts;\n switch (provider) {\n case 'log':\n return new LogTransport(logger);\n case 'resend':\n if (!apiKey) throw new Error(\"makeTransport: provider='resend' requires apiKey (OS_EMAIL_API_KEY)\");\n return new ResendTransport({ apiKey, ...(options as any) });\n case 'postmark':\n if (!apiKey) throw new Error(\"makeTransport: provider='postmark' requires apiKey (OS_EMAIL_API_KEY)\");\n return new PostmarkTransport({ apiKey, ...(options as any) });\n default:\n throw new Error(`makeTransport: unknown provider '${provider}'`);\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { EmailTemplateDefinition as EmailTemplate } from '@objectstack/spec/system';\n\n/**\n * Built-in auth email templates seeded into `sys_email_template` on\n * EmailServicePlugin startup. Each template is `isSystem: true` so\n * tenants may overlay subject/body but should not delete the row.\n *\n * Templates use `{{path.to.value}}` placeholders; `{{{...}}}` for\n * unescaped URLs (see template-engine.ts).\n *\n * Authoring conventions:\n * - Subject: plain, max ~80 chars, no markup.\n * - HTML body: single column, ~600px max width, inline styles only\n * (most clients strip <head>).\n * - Always include a plain-text fallback (good for spam scoring).\n * - Provide an `{{appName}}` variable everywhere for brand override.\n */\n\nconst baseStyles = 'font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;line-height:1.5;color:#1f2937';\nconst buttonStyles = 'display:inline-block;padding:12px 24px;background:#2563eb;color:#ffffff;text-decoration:none;border-radius:6px;font-weight:600';\nconst footerStyles = 'margin-top:32px;padding-top:16px;border-top:1px solid #e5e7eb;color:#6b7280;font-size:12px';\n\nfunction wrap(title: string, bodyHtml: string): string {\n return `<!doctype html><html><body style=\"${baseStyles};margin:0;padding:24px;background:#f9fafb\">\n<div style=\"max-width:560px;margin:0 auto;background:#ffffff;padding:32px;border-radius:8px;border:1px solid #e5e7eb\">\n<h1 style=\"margin:0 0 16px 0;font-size:20px;font-weight:600\">${title}</h1>\n${bodyHtml}\n<div style=\"${footerStyles}\">\nYou received this email because of activity on your {{appName}} account.<br>\nIf this wasn't you, you can safely ignore this message.\n</div>\n</div></body></html>`;\n}\n\nexport const AUTH_PASSWORD_RESET_TEMPLATE: EmailTemplate = {\n name: 'auth.password_reset',\n label: 'Password Reset',\n category: 'auth',\n locale: 'en-US',\n subject: 'Reset your {{appName}} password',\n bodyHtml: wrap('Reset your password', `\n<p>Hi {{user.name}},</p>\n<p>We received a request to reset the password for the account associated with <strong>{{user.email}}</strong>.</p>\n<p>Click the button below to choose a new password. This link expires in {{expiresInMinutes}} minutes.</p>\n<p style=\"margin:24px 0\"><a href=\"{{{resetUrl}}}\" style=\"${buttonStyles}\">Reset password</a></p>\n<p style=\"font-size:13px;color:#6b7280\">Or copy and paste this URL into your browser:<br><span style=\"word-break:break-all\">{{resetUrl}}</span></p>\n<p>If you didn't request this, no action is needed — your password stays the same.</p>\n`),\n bodyText: `Hi {{user.name}},\n\nWe received a request to reset the password for {{user.email}}.\n\nReset your password (link expires in {{expiresInMinutes}} minutes):\n{{resetUrl}}\n\nIf you didn't request this, ignore this email.`,\n variables: [\n { name: 'user.name', type: 'string', required: false, description: 'Recipient display name' },\n { name: 'user.email', type: 'string', required: true, description: 'Recipient email' },\n { name: 'resetUrl', type: 'url', required: true, description: 'Password reset URL' },\n { name: 'expiresInMinutes', type: 'number', required: false, description: 'Link TTL in minutes' },\n { name: 'appName', type: 'string', required: false, description: 'Product/app name (brand override)' },\n ],\n active: true,\n isSystem: true,\n description: 'Sent when a user requests a password reset via better-auth.',\n};\n\nexport const AUTH_VERIFY_EMAIL_TEMPLATE: EmailTemplate = {\n name: 'auth.verify_email',\n label: 'Verify Email Address',\n category: 'auth',\n locale: 'en-US',\n subject: 'Verify your {{appName}} email address',\n bodyHtml: wrap('Verify your email', `\n<p>Hi {{user.name}},</p>\n<p>Thanks for signing up for {{appName}}! Please confirm <strong>{{user.email}}</strong> belongs to you.</p>\n<p style=\"margin:24px 0\"><a href=\"{{{verificationUrl}}}\" style=\"${buttonStyles}\">Verify email</a></p>\n<p style=\"font-size:13px;color:#6b7280\">Or copy and paste this URL into your browser:<br><span style=\"word-break:break-all\">{{verificationUrl}}</span></p>\n`),\n bodyText: `Hi {{user.name}},\n\nPlease verify your email ({{user.email}}) by opening this link:\n{{verificationUrl}}`,\n variables: [\n { name: 'user.name', type: 'string', required: false },\n { name: 'user.email', type: 'string', required: true },\n { name: 'verificationUrl', type: 'url', required: true },\n { name: 'appName', type: 'string', required: false },\n ],\n active: true,\n isSystem: true,\n description: 'Sent when better-auth needs to verify a newly-registered email address.',\n};\n\nexport const AUTH_MAGIC_LINK_TEMPLATE: EmailTemplate = {\n name: 'auth.magic_link',\n label: 'Magic Link Sign-In',\n category: 'auth',\n locale: 'en-US',\n subject: 'Your {{appName}} sign-in link',\n bodyHtml: wrap('Sign in to {{appName}}', `\n<p>Click the button below to sign in. This link expires in {{expiresInMinutes}} minutes and may only be used once.</p>\n<p style=\"margin:24px 0\"><a href=\"{{{magicLinkUrl}}}\" style=\"${buttonStyles}\">Sign in</a></p>\n<p style=\"font-size:13px;color:#6b7280\">Or paste:<br><span style=\"word-break:break-all\">{{magicLinkUrl}}</span></p>\n`),\n bodyText: `Sign in to {{appName}} (expires in {{expiresInMinutes}} min):\n{{magicLinkUrl}}`,\n variables: [\n { name: 'magicLinkUrl', type: 'url', required: true },\n { name: 'expiresInMinutes', type: 'number', required: false },\n { name: 'appName', type: 'string', required: false },\n ],\n active: true,\n isSystem: true,\n description: 'Passwordless sign-in link sent by the magic-link plugin.',\n};\n\nexport const AUTH_INVITATION_TEMPLATE: EmailTemplate = {\n name: 'auth.invitation',\n label: 'Organization Invitation',\n category: 'auth',\n locale: 'en-US',\n subject: '{{inviter.name}} invited you to {{organization.name}}',\n bodyHtml: wrap('You have been invited', `\n<p><strong>{{inviter.name}}</strong> ({{inviter.email}}) has invited you to join <strong>{{organization.name}}</strong> on {{appName}} as <em>{{role}}</em>.</p>\n<p style=\"margin:24px 0\"><a href=\"{{{acceptUrl}}}\" style=\"${buttonStyles}\">Accept invitation</a></p>\n<p style=\"font-size:13px;color:#6b7280\">Or paste:<br><span style=\"word-break:break-all\">{{acceptUrl}}</span></p>\n`),\n bodyText: `{{inviter.name}} ({{inviter.email}}) invited you to join {{organization.name}} on {{appName}}.\n\nAccept: {{acceptUrl}}`,\n variables: [\n { name: 'inviter.name', type: 'string', required: false },\n { name: 'inviter.email', type: 'string', required: false },\n { name: 'organization.name', type: 'string', required: true },\n { name: 'role', type: 'string', required: false },\n { name: 'acceptUrl', type: 'url', required: true },\n { name: 'appName', type: 'string', required: false },\n ],\n active: true,\n isSystem: true,\n description: 'Sent by better-auth organization plugin when a user is invited to an org.',\n};\n\nexport const AUTH_TWO_FACTOR_OTP_TEMPLATE: EmailTemplate = {\n name: 'auth.two_factor_otp',\n label: 'Two-Factor Verification Code',\n category: 'auth',\n locale: 'en-US',\n subject: 'Your {{appName}} verification code',\n bodyHtml: wrap('Your verification code', `\n<p>Use this code to complete sign-in:</p>\n<p style=\"font-size:32px;font-weight:700;letter-spacing:6px;background:#f3f4f6;padding:16px;text-align:center;border-radius:6px;margin:24px 0\">{{otp}}</p>\n<p style=\"color:#6b7280;font-size:13px\">This code expires in {{expiresInMinutes}} minutes. If you didn't try to sign in, change your password — your account may be at risk.</p>\n`),\n bodyText: `Your {{appName}} verification code: {{otp}}\n(expires in {{expiresInMinutes}} minutes)`,\n variables: [\n { name: 'otp', type: 'string', required: true },\n { name: 'expiresInMinutes', type: 'number', required: false },\n { name: 'appName', type: 'string', required: false },\n ],\n active: true,\n isSystem: true,\n description: 'Time-based OTP delivered for two-factor / email-OTP login.',\n};\n\nexport const BUILTIN_AUTH_TEMPLATES: EmailTemplate[] = [\n AUTH_PASSWORD_RESET_TEMPLATE,\n AUTH_VERIFY_EMAIL_TEMPLATE,\n AUTH_MAGIC_LINK_TEMPLATE,\n AUTH_INVITATION_TEMPLATE,\n AUTH_TWO_FACTOR_OTP_TEMPLATE,\n];\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@objectstack/plugin-email",
|
|
3
|
+
"version": "4.0.1",
|
|
4
|
+
"license": "Apache-2.0",
|
|
5
|
+
"description": "Email service plugin for ObjectStack — IEmailService + transport-pluggable outbound delivery with sys_email persistence.",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@objectstack/core": "4.1.0",
|
|
17
|
+
"@objectstack/platform-objects": "4.1.0",
|
|
18
|
+
"@objectstack/spec": "4.1.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^25.9.1",
|
|
22
|
+
"typescript": "^6.0.3",
|
|
23
|
+
"vitest": "^4.1.7"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"objectstack",
|
|
27
|
+
"plugin",
|
|
28
|
+
"email",
|
|
29
|
+
"smtp"
|
|
30
|
+
],
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsup --config ../../../tsup.config.ts",
|
|
33
|
+
"test": "vitest run --passWithNoTests"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
|
|
3
|
+
import type { Plugin, PluginContext } from '@objectstack/core';
|
|
4
|
+
import type { IDataEngine } from '@objectstack/spec/contracts';
|
|
5
|
+
import type {
|
|
6
|
+
IEmailTransport,
|
|
7
|
+
EmailAddress,
|
|
8
|
+
} from '@objectstack/spec/contracts';
|
|
9
|
+
import { SysEmail, SysEmailTemplate } from '@objectstack/platform-objects/audit';
|
|
10
|
+
import { EmailService, LogTransport, type EmailPersistence, type TemplateLoader, type EmailTemplateRow } from './email-service.js';
|
|
11
|
+
import { makeTransport } from './transports/index.js';
|
|
12
|
+
import { BUILTIN_AUTH_TEMPLATES } from './templates/auth-templates.js';
|
|
13
|
+
import type { EmailTemplateDefinition as EmailTemplate } from '@objectstack/spec/system';
|
|
14
|
+
|
|
15
|
+
const SYSTEM_CTX = { isSystem: true, roles: [], permissions: [] } as const;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Plugin configuration.
|
|
19
|
+
*/
|
|
20
|
+
export interface EmailServicePluginOptions {
|
|
21
|
+
/**
|
|
22
|
+
* Pluggable delivery transport. When omitted the plugin builds one
|
|
23
|
+
* from `provider`/`apiKey`; if both omitted, falls back to
|
|
24
|
+
* `LogTransport` (no real send).
|
|
25
|
+
*/
|
|
26
|
+
transport?: IEmailTransport;
|
|
27
|
+
/** Provider tag — `'log' | 'resend' | 'postmark'`. Default `'log'`. */
|
|
28
|
+
provider?: 'log' | 'resend' | 'postmark';
|
|
29
|
+
/** API key for resend/postmark. */
|
|
30
|
+
apiKey?: string;
|
|
31
|
+
/** Provider-specific extra options (e.g. Postmark messageStream). */
|
|
32
|
+
providerOptions?: Record<string, unknown>;
|
|
33
|
+
/** Default `From` address applied when `input.from` is omitted. */
|
|
34
|
+
defaultFrom?: EmailAddress;
|
|
35
|
+
/** Persist each attempt to sys_email. Default true when ObjectQL engine present. */
|
|
36
|
+
persist?: boolean;
|
|
37
|
+
/** Retry attempts on transport throw. Default 0. */
|
|
38
|
+
retries?: number;
|
|
39
|
+
/** Default template render context (merged into every sendTemplate call). */
|
|
40
|
+
defaultTemplateContext?: Record<string, unknown>;
|
|
41
|
+
/** Seed built-in auth templates into sys_email_template on startup. Default true. */
|
|
42
|
+
seedTemplates?: boolean;
|
|
43
|
+
/** Additional templates seeded alongside the built-ins. */
|
|
44
|
+
templates?: EmailTemplate[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* EmailServicePlugin — registers the `email` service.
|
|
49
|
+
*
|
|
50
|
+
* Lifecycle:
|
|
51
|
+
* - `init`: register sys_email + sys_email_template via manifest;
|
|
52
|
+
* build transport (config → provider+apiKey → LogTransport fallback);
|
|
53
|
+
* register a transport-only EmailService so dependents can resolve it.
|
|
54
|
+
* - `start` (kernel:ready): wire ObjectQL-backed sys_email persistence
|
|
55
|
+
* + sys_email_template TemplateLoader; seed built-in auth templates
|
|
56
|
+
* (upsert by `(name, locale)`).
|
|
57
|
+
*/
|
|
58
|
+
export class EmailServicePlugin implements Plugin {
|
|
59
|
+
name = 'com.objectstack.service.email';
|
|
60
|
+
version = '1.0.0';
|
|
61
|
+
type = 'standard';
|
|
62
|
+
dependencies = ['com.objectstack.engine.objectql'];
|
|
63
|
+
|
|
64
|
+
private readonly options: EmailServicePluginOptions;
|
|
65
|
+
private service?: EmailService;
|
|
66
|
+
|
|
67
|
+
constructor(options: EmailServicePluginOptions = {}) {
|
|
68
|
+
this.options = options;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private resolveTransport(ctx: PluginContext): IEmailTransport {
|
|
72
|
+
if (this.options.transport) return this.options.transport;
|
|
73
|
+
const provider = this.options.provider ?? 'log';
|
|
74
|
+
if (provider === 'log') return new LogTransport(ctx.logger);
|
|
75
|
+
return makeTransport({
|
|
76
|
+
provider,
|
|
77
|
+
apiKey: this.options.apiKey,
|
|
78
|
+
options: this.options.providerOptions,
|
|
79
|
+
logger: ctx.logger,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async init(ctx: PluginContext): Promise<void> {
|
|
84
|
+
// Register sys_email + sys_email_template via manifest service.
|
|
85
|
+
ctx.getService<{ register(m: any): void }>('manifest').register({
|
|
86
|
+
id: 'com.objectstack.service.email',
|
|
87
|
+
name: 'Email Service',
|
|
88
|
+
version: '1.0.0',
|
|
89
|
+
type: 'plugin',
|
|
90
|
+
scope: 'system',
|
|
91
|
+
defaultDatasource: 'cloud',
|
|
92
|
+
namespace: 'sys',
|
|
93
|
+
objects: [SysEmail, SysEmailTemplate],
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const transport = this.resolveTransport(ctx);
|
|
97
|
+
if (!this.options.transport && (this.options.provider ?? 'log') === 'log') {
|
|
98
|
+
ctx.logger.info(
|
|
99
|
+
'EmailServicePlugin: no transport configured — using LogTransport (mail will NOT be sent)',
|
|
100
|
+
);
|
|
101
|
+
} else {
|
|
102
|
+
ctx.logger.info(
|
|
103
|
+
`EmailServicePlugin: using '${this.options.provider ?? 'log'}' provider`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Persistence + templateLoader are wired in `start` once the
|
|
108
|
+
// ObjectQL engine is available; here we register the service
|
|
109
|
+
// synchronously so dependents can resolve it.
|
|
110
|
+
this.service = new EmailService({
|
|
111
|
+
transport,
|
|
112
|
+
defaultFrom: this.options.defaultFrom,
|
|
113
|
+
retries: this.options.retries,
|
|
114
|
+
defaultTemplateContext: this.options.defaultTemplateContext,
|
|
115
|
+
logger: ctx.logger,
|
|
116
|
+
});
|
|
117
|
+
ctx.registerService('email', this.service);
|
|
118
|
+
ctx.logger.info('EmailServicePlugin: email service registered');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async start(ctx: PluginContext): Promise<void> {
|
|
122
|
+
ctx.hook('kernel:ready', async () => {
|
|
123
|
+
let engine: IDataEngine | null = null;
|
|
124
|
+
try { engine = ctx.getService<IDataEngine>('objectql'); }
|
|
125
|
+
catch { try { engine = ctx.getService<IDataEngine>('data'); } catch { /* ignore */ } }
|
|
126
|
+
if (!engine || !this.service) return;
|
|
127
|
+
|
|
128
|
+
// ── Bind to the `mail` settings namespace (Phase 1) ──────────────
|
|
129
|
+
// Allows the admin UI to live-update SMTP/provider/from-address
|
|
130
|
+
// without restarting the process. Env-locked fields still win at
|
|
131
|
+
// the resolver level, so config-via-env keeps its precedence.
|
|
132
|
+
try {
|
|
133
|
+
const settings = ctx.getService<any>('settings');
|
|
134
|
+
if (settings && typeof settings.createClient === 'function') {
|
|
135
|
+
const applySettings = async () => {
|
|
136
|
+
try {
|
|
137
|
+
const payload = await settings.getNamespace('mail');
|
|
138
|
+
const values: Record<string, unknown> = {};
|
|
139
|
+
for (const [k, v] of Object.entries(payload.values as Record<string, any>)) {
|
|
140
|
+
values[k] = v?.value;
|
|
141
|
+
}
|
|
142
|
+
this.applyMailSettings(values, ctx);
|
|
143
|
+
} catch (err: any) {
|
|
144
|
+
ctx.logger.warn('EmailServicePlugin: failed to apply mail settings: ' + (err?.message ?? err));
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
await applySettings();
|
|
148
|
+
// Subscribe to namespace changes; rebuild on every update.
|
|
149
|
+
if (typeof settings.subscribe === 'function') {
|
|
150
|
+
settings.subscribe('mail', () => {
|
|
151
|
+
void applySettings();
|
|
152
|
+
});
|
|
153
|
+
ctx.logger.info('EmailServicePlugin: bound to settings:changed for namespace=mail');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Register the `mail/test` action handler so saving + sending
|
|
157
|
+
// a test email actually exercises the live transport.
|
|
158
|
+
if (typeof settings.registerAction === 'function') {
|
|
159
|
+
const svc = this.service;
|
|
160
|
+
settings.registerAction('mail', 'test', async ({ values, ctx: actionCtx }: any) => {
|
|
161
|
+
const to = (actionCtx?.body?.to as string | undefined)
|
|
162
|
+
?? (values.from_email as string | undefined);
|
|
163
|
+
if (!to) {
|
|
164
|
+
return { ok: false, severity: 'error', message: 'Provide a "to" address (or set from_email).' };
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
const result = await svc.send({
|
|
168
|
+
to,
|
|
169
|
+
from: values.from_email ? {
|
|
170
|
+
address: String(values.from_email),
|
|
171
|
+
name: values.from_name ? String(values.from_name) : undefined,
|
|
172
|
+
} : undefined,
|
|
173
|
+
subject: 'ObjectStack mail test',
|
|
174
|
+
text: 'This is a test email from the ObjectStack settings page.',
|
|
175
|
+
});
|
|
176
|
+
if (result.status === 'failed') {
|
|
177
|
+
return { ok: false, severity: 'error', message: result.error ?? 'Send failed.' };
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
ok: true,
|
|
181
|
+
severity: 'info',
|
|
182
|
+
message: `Sent test email to ${to} (id=${result.id}).`,
|
|
183
|
+
};
|
|
184
|
+
} catch (err: any) {
|
|
185
|
+
return { ok: false, severity: 'error', message: err?.message ?? String(err) };
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
} catch {
|
|
191
|
+
// settings service not registered — env/constructor opts remain authoritative.
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const persistence: EmailPersistence | undefined = this.options.persist === false
|
|
195
|
+
? undefined
|
|
196
|
+
: {
|
|
197
|
+
async insert(row) {
|
|
198
|
+
const created = await (engine as any).insert('sys_email', row, {
|
|
199
|
+
context: SYSTEM_CTX,
|
|
200
|
+
});
|
|
201
|
+
return created?.id ? { id: String(created.id) } : { id: String(row.id) };
|
|
202
|
+
},
|
|
203
|
+
async update(id, patch) {
|
|
204
|
+
await (engine as any).update('sys_email', { id, ...patch }, {
|
|
205
|
+
context: SYSTEM_CTX,
|
|
206
|
+
});
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const templateLoader: TemplateLoader = {
|
|
211
|
+
async load(name, locale) {
|
|
212
|
+
const where: Record<string, unknown> = { name };
|
|
213
|
+
if (locale) where.locale = locale;
|
|
214
|
+
const rows = await (engine as any).find('sys_email_template', {
|
|
215
|
+
where,
|
|
216
|
+
limit: 1,
|
|
217
|
+
context: SYSTEM_CTX,
|
|
218
|
+
});
|
|
219
|
+
const row = Array.isArray(rows) ? rows[0] : (rows as any)?.data?.[0];
|
|
220
|
+
return (row as EmailTemplateRow) || null;
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// Mutate the existing service instance so consumers that already
|
|
225
|
+
// captured a reference (e.g. AuthManager) see the upgrade.
|
|
226
|
+
if (persistence) this.service.setPersistence(persistence);
|
|
227
|
+
this.service.setTemplateLoader(templateLoader);
|
|
228
|
+
ctx.logger.info('EmailServicePlugin: sys_email persistence + template loader enabled');
|
|
229
|
+
|
|
230
|
+
// Bind 'email.send.async' queue subscriber for durable, retry-on-failure delivery.
|
|
231
|
+
// Producers: `queue.publish('email.send.async', sendInput, { maxAttempts: 5, backoff: {...} })`
|
|
232
|
+
// The queue handles retry / DLQ via sys_job_queue.
|
|
233
|
+
try {
|
|
234
|
+
const queue: any = ctx.getService<any>('queue');
|
|
235
|
+
if (queue && typeof queue.subscribe === 'function' && this.service) {
|
|
236
|
+
const svc = this.service;
|
|
237
|
+
await queue.subscribe('email.send.async', async (msg: any) => {
|
|
238
|
+
const result = await svc.send(msg.data);
|
|
239
|
+
if (result.status === 'failed') {
|
|
240
|
+
// Force the queue to retry / DLQ by throwing
|
|
241
|
+
throw new Error(result.error ?? 'email send failed');
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
ctx.logger.info('EmailServicePlugin: subscribed to email.send.async queue');
|
|
245
|
+
}
|
|
246
|
+
} catch (err) {
|
|
247
|
+
ctx.logger.warn('EmailServicePlugin: email.send.async subscription failed', err as any);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Seed built-in + user-provided templates (upsert by name+locale).
|
|
251
|
+
if (this.options.seedTemplates !== false) {
|
|
252
|
+
const all = [
|
|
253
|
+
...BUILTIN_AUTH_TEMPLATES,
|
|
254
|
+
...(this.options.templates ?? []),
|
|
255
|
+
];
|
|
256
|
+
for (const tpl of all) {
|
|
257
|
+
try { await this.upsertTemplate(engine!, tpl); }
|
|
258
|
+
catch (err: any) {
|
|
259
|
+
ctx.logger.warn(`EmailServicePlugin: seed template failed: ${tpl.name} ${tpl.locale}`, err?.message || err);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
ctx.logger.info(`EmailServicePlugin: seeded ${all.length} template row(s)`);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Translate the `mail` settings namespace snapshot into a transport
|
|
269
|
+
* and `defaultFrom`, then hot-swap them on the running EmailService.
|
|
270
|
+
*
|
|
271
|
+
* Behaviour:
|
|
272
|
+
* - `provider = 'log' | 'smtp'` keeps the LogTransport (real SMTP
|
|
273
|
+
* delivery requires `@objectstack/plugin-mail-smtp`, which is not
|
|
274
|
+
* a dependency of this package). The from-address is still applied.
|
|
275
|
+
* - `provider = 'resend' | 'postmark'` rebuilds the transport using
|
|
276
|
+
* `api_key` from settings. If `api_key` is missing the swap is
|
|
277
|
+
* skipped and a warning is logged — the previous transport stays.
|
|
278
|
+
*
|
|
279
|
+
* Env-locked fields (handled in SettingsService.get) still resolve
|
|
280
|
+
* before this method ever sees them, so an env override transparently
|
|
281
|
+
* wins.
|
|
282
|
+
*/
|
|
283
|
+
private applyMailSettings(values: Record<string, unknown>, ctx: PluginContext): void {
|
|
284
|
+
if (!this.service) return;
|
|
285
|
+
|
|
286
|
+
const fromEmail = typeof values.from_email === 'string' ? values.from_email : undefined;
|
|
287
|
+
const fromName = typeof values.from_name === 'string' ? values.from_name : undefined;
|
|
288
|
+
if (fromEmail) this.service.setDefaultFrom({ address: fromEmail, name: fromName });
|
|
289
|
+
|
|
290
|
+
const provider = String(values.provider ?? 'smtp');
|
|
291
|
+
if (provider === 'smtp' || provider === 'log') {
|
|
292
|
+
// No SMTP transport ships in core; settings-only edits become
|
|
293
|
+
// a no-op for transport but still apply `defaultFrom`. Users
|
|
294
|
+
// wanting real SMTP install `@objectstack/plugin-mail-smtp`
|
|
295
|
+
// and configure it via constructor opts.
|
|
296
|
+
ctx.logger.info(
|
|
297
|
+
`EmailServicePlugin: mail settings applied (provider=${provider}, from=${fromEmail ?? '∅'}); transport unchanged.`,
|
|
298
|
+
);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const apiKey = typeof values.api_key === 'string' ? values.api_key : undefined;
|
|
303
|
+
if (!apiKey) {
|
|
304
|
+
ctx.logger.warn(
|
|
305
|
+
`EmailServicePlugin: provider='${provider}' selected but api_key is empty — transport NOT rebuilt.`,
|
|
306
|
+
);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
const transport = makeTransport({
|
|
312
|
+
provider: provider as 'resend' | 'postmark',
|
|
313
|
+
apiKey,
|
|
314
|
+
logger: ctx.logger,
|
|
315
|
+
});
|
|
316
|
+
this.service.setTransport(transport);
|
|
317
|
+
ctx.logger.info(`EmailServicePlugin: transport rebuilt from settings (provider=${provider}).`);
|
|
318
|
+
} catch (err: any) {
|
|
319
|
+
ctx.logger.warn('EmailServicePlugin: failed to rebuild transport: ' + (err?.message ?? err));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private async upsertTemplate(engine: IDataEngine, tpl: EmailTemplate): Promise<void> {
|
|
324
|
+
const row = {
|
|
325
|
+
name: tpl.name,
|
|
326
|
+
label: tpl.label,
|
|
327
|
+
category: tpl.category,
|
|
328
|
+
locale: tpl.locale,
|
|
329
|
+
subject: tpl.subject,
|
|
330
|
+
body_html: tpl.bodyHtml,
|
|
331
|
+
...(tpl.bodyText ? { body_text: tpl.bodyText } : {}),
|
|
332
|
+
...(tpl.fromOverride?.address ? {
|
|
333
|
+
from_address: tpl.fromOverride.address,
|
|
334
|
+
...(tpl.fromOverride.name ? { from_name: tpl.fromOverride.name } : {}),
|
|
335
|
+
} : {}),
|
|
336
|
+
...(tpl.replyTo ? { reply_to: tpl.replyTo } : {}),
|
|
337
|
+
active: tpl.active,
|
|
338
|
+
is_system: tpl.isSystem,
|
|
339
|
+
...(tpl.description ? { description: tpl.description } : {}),
|
|
340
|
+
...(tpl.variables?.length ? { variables_json: JSON.stringify(tpl.variables) } : {}),
|
|
341
|
+
};
|
|
342
|
+
const existing = await (engine as any).find('sys_email_template', {
|
|
343
|
+
where: { name: tpl.name, locale: tpl.locale },
|
|
344
|
+
limit: 1,
|
|
345
|
+
context: SYSTEM_CTX,
|
|
346
|
+
});
|
|
347
|
+
const existingRow = Array.isArray(existing) ? existing[0] : (existing as any)?.data?.[0];
|
|
348
|
+
if (existingRow?.id) {
|
|
349
|
+
// Only re-seed if the existing row is system-managed (is_system=true);
|
|
350
|
+
// never overwrite a tenant-customised row.
|
|
351
|
+
if (existingRow.is_system === false) return;
|
|
352
|
+
await (engine as any).update('sys_email_template', { id: existingRow.id, ...row }, {
|
|
353
|
+
context: SYSTEM_CTX,
|
|
354
|
+
});
|
|
355
|
+
} else {
|
|
356
|
+
await (engine as any).insert('sys_email_template', row, {
|
|
357
|
+
context: SYSTEM_CTX,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|