alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
217 lines (216 loc) • 6.33 kB
JavaScript
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