UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

217 lines (216 loc) 6.33 kB
import { $atom, $hook, $inject, $module, $state, AlephaError, KIND, Primitive, createPrimitive, z } from "alepha"; import { $logger } from "alepha/logger"; import { FileSystemProvider } from "alepha/system"; //#region ../../src/email/core/providers/EmailProvider.ts /** * Email provider interface. * * All methods are asynchronous and return promises. */ var EmailProvider = class {}; //#endregion //#region ../../src/email/core/providers/MemoryEmailProvider.ts var MemoryEmailProvider = class { log = $logger(); records = []; async send(options) { const { to, subject, body } = options; this.log.debug("Sending email to memory store", { to, subject }); for (const recipient of Array.isArray(to) ? to : [to]) this.records.push({ to: recipient, subject, body, sentAt: /* @__PURE__ */ new Date() }); } /** * Get the last email sent (for testing purposes). */ get last() { return this.records[this.records.length - 1]; } }; //#endregion //#region ../../src/email/core/primitives/$email.ts const $email = (options = {}) => createPrimitive(EmailPrimitive, options); /** * Email primitive for sending emails through various providers. * * The primitive's `name` identifies the channel in the `email:sending` / * `email:sent` hooks; it does not select a template — `send()` expects an * already-rendered `subject` and `body`. * * Usage: * ```typescript * class MyService { * protected readonly welcomeEmail = $email({ name: "welcome" }); * * async sendWelcome(userEmail: string, userName: string) { * await this.welcomeEmail.send({ * to: userEmail, * subject: "Welcome!", * body: `<p>Hello ${userName}!</p>` * }); * } * } * ``` */ var EmailPrimitive = class extends Primitive { provider = this.$provider(); get name() { return this.options.name ?? `${this.config.propertyKey}`; } /** * Send an email using the configured provider. */ async send(options) { await this.alepha.events.emit("email:sending", { to: options.to, template: this.name, variables: {}, provider: this.provider, abort: () => { throw new AlephaError("Email sending aborted by hook"); } }); await this.provider.send(options); await this.alepha.events.emit("email:sent", { to: options.to, template: this.name, provider: this.provider }); } $provider() { if (!this.options.provider) return this.alepha.inject(EmailProvider); if (this.options.provider === "memory") return this.alepha.inject(MemoryEmailProvider); return this.alepha.inject(this.options.provider); } }; $email[KIND] = EmailPrimitive; //#endregion //#region ../../src/email/core/errors/EmailError.ts var EmailError = class extends AlephaError { constructor(message, cause) { super(message); this.name = "EmailError"; this.cause = cause; } }; //#endregion //#region ../../src/email/core/providers/LocalEmailProvider.ts /** * Local email provider configuration atom */ const localEmailOptions = $atom({ name: "alepha.email.local.options", schema: z.object({ directory: z.string().describe("Directory path where email files will be stored") }), default: { directory: "node_modules/.alepha/emails" } }); var LocalEmailProvider = class { log = $logger(); fs = $inject(FileSystemProvider); options = $state(localEmailOptions); get directory() { return this.options.directory; } onStart = $hook({ on: "start", handler: async () => { try { await this.fs.mkdir(this.directory, { recursive: true }); this.log.info("Email directory OK", { at: this.directory }); } catch (error) { const message = `Failed to create email directory: ${error instanceof Error ? error.message : String(error)}`; this.log.error(message, { at: this.directory }); throw new EmailError(message, error instanceof Error ? error : void 0); } } }); async send(options) { const { to, subject, body } = options; this.log.debug("Sending email to local file", { to, subject, directory: this.directory }); try { const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-"); for (const recipient of Array.isArray(to) ? to : [to]) { const filename = `${recipient.replace(/[^a-zA-Z0-9@.-]/g, "_")},${timestamp}.eml.json`; const filepath = this.fs.join(this.directory, filename); const content = this.createEmailJson({ to: recipient, subject, body }); await this.fs.writeFile(filepath, JSON.stringify(content, null, 2)); this.log.info("Email saved to local file", { filepath, to, subject }); } } catch (error) { const message = `Failed to save email to local file: ${error instanceof Error ? error.message : String(error)}`; this.log.error(message, { to, subject, directory: this.directory }); throw new EmailError(message, error instanceof Error ? error : void 0); } } createEmailJson(options) { return { to: options.to, subject: options.subject, body: options.body, sentAt: (/* @__PURE__ */ new Date()).toISOString() }; } }; //#endregion //#region ../../src/email/core/index.ts /** * Email delivery over pluggable providers. * * **Features:** * - `$email` declares a named send channel; the name is surfaced to the * `email:sending` / `email:sent` hooks (as their `template` field) for * auditing and interception * - Multiple recipients * - Local file provider for development * - Memory provider for testing * * There is **no template rendering**: `send()` takes an already-rendered * `subject` and `body` — bring your own templating if you need it. * * For SMTP support, use `AlephaEmailSmtp` from `alepha/email/smtp`. * For Brevo support, use `AlephaEmailBrevo` from `alepha/email/brevo`. * * @module alepha.email */ const AlephaEmail = $module({ name: "alepha.email", primitives: [$email], services: [EmailProvider], variants: [MemoryEmailProvider, LocalEmailProvider], register: (alepha) => { if (alepha.isTest()) alepha.with({ optional: true, provide: EmailProvider, use: MemoryEmailProvider }); else alepha.with({ optional: true, provide: EmailProvider, use: LocalEmailProvider }); } }); //#endregion export { $email, AlephaEmail, EmailError, EmailPrimitive, EmailProvider, LocalEmailProvider, MemoryEmailProvider, localEmailOptions }; //# sourceMappingURL=index.js.map