@veap/mail 0.1.0 → 0.1.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/README.md CHANGED
@@ -1,5 +1,6 @@
1
- [![CI](https://github.com/veapjs/veap-mail/actions/workflows/ci.yml/badge.svg)](https://github.com/veapjs/veap-mail/actions/workflows/ci.yml)
2
- [![Release](https://github.com/veapjs/veap-mail/actions/workflows/release.yml/badge.svg)](https://github.com/veapjs/veap-mail/actions/workflows/release.yml)
1
+ [![CI](https://github.com/veapjs/veap-mail/actions/workflows/CI.yml/badge.svg)](https://github.com/veapjs/veap-mail/actions/workflows/CI.yml)
2
+ [![Release](https://github.com/veapjs/veap-mail/actions/workflows/RELEASE.yml/badge.svg)](https://github.com/veapjs/veap-mail/actions/workflows/RELEASE.yml)
3
+ [![Version](https://img.shields.io/npm/v/@veap/mail?style=flat&colorA=000000&colorB=000000)](https://www.npmjs.com/package/@veap/mail)
3
4
 
4
5
  # @veap/mail
5
6
 
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Mail configuration options supported in veap.config.ts.
3
+ */
4
+ export interface MailConfig {
5
+ /**
6
+ * Transport driver: "smtp" (default), "console", or "custom".
7
+ */
8
+ transport?: "smtp" | "console" | "custom" | string;
9
+ /**
10
+ * Nodemailer well-known service (e.g. "gmail", "sendgrid").
11
+ */
12
+ service?: string;
13
+ /**
14
+ * SMTP server hostname.
15
+ */
16
+ host?: string;
17
+ /**
18
+ * SMTP server port (e.g. 587, 465, 25, 1025).
19
+ */
20
+ port?: number | string;
21
+ /**
22
+ * Use TLS/SSL directly (default: true for port 465, false otherwise).
23
+ */
24
+ secure?: boolean;
25
+ /**
26
+ * Ignore TLS certificate issues (useful for local dev/test servers).
27
+ */
28
+ ignoreTLS?: boolean;
29
+ /**
30
+ * SMTP authentication username.
31
+ */
32
+ username?: string;
33
+ /**
34
+ * SMTP authentication password.
35
+ */
36
+ password?: string;
37
+ /**
38
+ * Default sender address (e.g. "noreply@example.com" or "Acme <noreply@example.com>").
39
+ */
40
+ from?: string;
41
+ /**
42
+ * Default sender name (e.g. "Acme").
43
+ */
44
+ fromName?: string;
45
+ }
46
+ declare module "@veap/kernel" {
47
+ interface ApplicationBuilder {
48
+ /**
49
+ * Registers the communication/mail service provider into the application.
50
+ */
51
+ withMail(): this;
52
+ /**
53
+ * Alias for `withMail()`.
54
+ */
55
+ withCommunication(): this;
56
+ }
57
+ interface VeapConfig {
58
+ /**
59
+ * Communication and mail transport configuration.
60
+ */
61
+ mail?: MailConfig;
62
+ }
63
+ }
@@ -0,0 +1,8 @@
1
+ import { ApplicationBuilder } from "@veap/kernel";
2
+ import { CommunicationServiceProvider } from "./provider.js";
3
+ ApplicationBuilder.prototype.withMail = function () {
4
+ return this.use(CommunicationServiceProvider);
5
+ };
6
+ ApplicationBuilder.prototype.withCommunication = function () {
7
+ return this.use(CommunicationServiceProvider);
8
+ };
package/dist/context.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { IMailer } from "./mailer";
1
+ import type { IMailer } from "./mailer.js";
2
2
  /**
3
3
  * Typed boundary between the communication use cases and their consumers
4
4
  * (facades, application services). The same pattern as
package/dist/index.d.ts CHANGED
@@ -1,10 +1,11 @@
1
- export * from "./context";
2
- export * from "./mail";
3
- export type { MailMessage as SendMailOptions } from "./mail-message";
4
- export * from "./mail-message";
5
- export * from "./mailer";
6
- export * from "./provider";
7
- export * from "./providers/console.provider";
8
- export * from "./providers/nodemailer.provider";
9
- export { NodemailerMailService as MailService } from "./providers/nodemailer.provider";
10
- export * from "./translator";
1
+ export * from "./composition.js";
2
+ export * from "./context.js";
3
+ export * from "./mail.js";
4
+ export type { MailMessage as SendMailOptions } from "./mail-message.js";
5
+ export * from "./mail-message.js";
6
+ export * from "./mailer.js";
7
+ export * from "./provider.js";
8
+ export * from "./providers/console.provider.js";
9
+ export * from "./providers/nodemailer.provider.js";
10
+ export { NodemailerMailService as MailService } from "./providers/nodemailer.provider.js";
11
+ export * from "./translator.js";
package/dist/index.js CHANGED
@@ -1,9 +1,10 @@
1
- export * from "./context";
2
- export * from "./mail";
3
- export * from "./mail-message";
4
- export * from "./mailer";
5
- export * from "./provider";
6
- export * from "./providers/console.provider";
7
- export * from "./providers/nodemailer.provider";
8
- export { NodemailerMailService as MailService } from "./providers/nodemailer.provider";
9
- export * from "./translator";
1
+ export * from "./composition.js";
2
+ export * from "./context.js";
3
+ export * from "./mail.js";
4
+ export * from "./mail-message.js";
5
+ export * from "./mailer.js";
6
+ export * from "./provider.js";
7
+ export * from "./providers/console.provider.js";
8
+ export * from "./providers/nodemailer.provider.js";
9
+ export { NodemailerMailService as MailService } from "./providers/nodemailer.provider.js";
10
+ export * from "./translator.js";
package/dist/mail.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { MailMessage } from "./mail-message";
1
+ import type { MailMessage } from "./mail-message.js";
2
2
  /**
3
3
  * Mailables - the domain vocabulary of email.
4
4
  *
package/dist/mail.js CHANGED
@@ -1,5 +1,5 @@
1
- import { communicationContext } from "./context";
2
- import { getMailTranslator } from "./translator";
1
+ import { communicationContext } from "./context.js";
2
+ import { getMailTranslator } from "./translator.js";
3
3
  /**
4
4
  * Mailables - the domain vocabulary of email.
5
5
  *
package/dist/mailer.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import type { MailMessage } from "./mail-message";
1
+ import type { Token } from "@veap/kernel";
2
+ import type { MailMessage } from "./mail-message.js";
2
3
  /**
3
4
  * Mailer port.
4
5
  *
@@ -22,7 +23,11 @@ export interface IMailer {
22
23
  * boundaries, the same reason the container keys class tokens with
23
24
  * `Symbol.for(...)`.
24
25
  */
25
- export declare const MAILER: unique symbol;
26
+ export declare const MAILER: Token<IMailer>;
27
+ /**
28
+ * Backward compatibility alias for the mailer port token.
29
+ */
30
+ export declare const MAIL_SERVICE: Token<IMailer>;
26
31
  /**
27
32
  * Injection token for a custom transport supplied by the host application.
28
33
  *
@@ -30,4 +35,4 @@ export declare const MAILER: unique symbol;
30
35
  * this token (e.g. SES, Postmark, Resend, an outbox pattern…) to route all
31
36
  * mail through your own delivery system without touching the framework.
32
37
  */
33
- export declare const CUSTOM_MAILER: unique symbol;
38
+ export declare const CUSTOM_MAILER: Token<IMailer>;
package/dist/mailer.js CHANGED
@@ -6,6 +6,10 @@
6
6
  * `Symbol.for(...)`.
7
7
  */
8
8
  export const MAILER = Symbol.for("veap:communication:mailer");
9
+ /**
10
+ * Backward compatibility alias for the mailer port token.
11
+ */
12
+ export const MAIL_SERVICE = MAILER;
9
13
  /**
10
14
  * Injection token for a custom transport supplied by the host application.
11
15
  *
@@ -6,8 +6,8 @@ import { ServiceProvider } from "@veap/kernel";
6
6
  * and binds it into `CommunicationContext`, so facades and services never
7
7
  * look transports up themselves:
8
8
  *
9
- * - `smtp` (default) - Nodemailer (`MAIL_SERVICE`, `MAIL_USERNAME`/`MAIL_PASSWORD`
10
- * or `GOOGLE_SMTP_APP_*`, `MAIL_FROM_ADDRESS`)
9
+ * - `smtp` (default) - Nodemailer (`MAIL_SERVICE`, `MAIL_HOST`, `MAIL_PORT`,
10
+ * `MAIL_USERNAME`/`MAIL_PASSWORD` or `GOOGLE_SMTP_APP_*`, `MAIL_FROM_ADDRESS`)
11
11
  * - `console` - logs the message; dev/test with zero mail configuration
12
12
  * - `custom` - resolves `CUSTOM_MAILER`; bring your own SES/Postmark/Resend…
13
13
  */
package/dist/provider.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { ConfigService, logger, ServiceProvider } from "@veap/kernel";
2
- import { bindCommunicationContext } from "./context";
3
- import { CUSTOM_MAILER, MAILER } from "./mailer";
4
- import { ConsoleMailService } from "./providers/console.provider";
5
- import { NodemailerMailService } from "./providers/nodemailer.provider";
6
- import { setMailTranslatorFactory } from "./translator";
2
+ import { bindCommunicationContext } from "./context.js";
3
+ import { CUSTOM_MAILER, MAILER } from "./mailer.js";
4
+ import { ConsoleMailService } from "./providers/console.provider.js";
5
+ import { NodemailerMailService } from "./providers/nodemailer.provider.js";
6
+ import { setMailTranslatorFactory } from "./translator.js";
7
7
  /**
8
8
  * Composition root of the communication context.
9
9
  *
@@ -11,8 +11,8 @@ import { setMailTranslatorFactory } from "./translator";
11
11
  * and binds it into `CommunicationContext`, so facades and services never
12
12
  * look transports up themselves:
13
13
  *
14
- * - `smtp` (default) - Nodemailer (`MAIL_SERVICE`, `MAIL_USERNAME`/`MAIL_PASSWORD`
15
- * or `GOOGLE_SMTP_APP_*`, `MAIL_FROM_ADDRESS`)
14
+ * - `smtp` (default) - Nodemailer (`MAIL_SERVICE`, `MAIL_HOST`, `MAIL_PORT`,
15
+ * `MAIL_USERNAME`/`MAIL_PASSWORD` or `GOOGLE_SMTP_APP_*`, `MAIL_FROM_ADDRESS`)
16
16
  * - `console` - logs the message; dev/test with zero mail configuration
17
17
  * - `custom` - resolves `CUSTOM_MAILER`; bring your own SES/Postmark/Resend…
18
18
  */
@@ -25,24 +25,44 @@ export class CommunicationServiceProvider extends ServiceProvider {
25
25
  // selects another transport.
26
26
  this.container.register({
27
27
  token: MAILER,
28
- useFactory: (config) => new NodemailerMailService({
29
- getService: () => config.get("MAIL_SERVICE"),
30
- getUsername: () => config.get("GOOGLE_SMTP_APP_USERNAME") ??
31
- config.get("MAIL_USERNAME"),
32
- getPassword: () => config.get("GOOGLE_SMTP_APP_PASSWORD") ??
33
- config.get("MAIL_PASSWORD"),
34
- getFromAddress: () => config.get("MAIL_FROM_ADDRESS"),
35
- }),
28
+ useFactory: (config) => {
29
+ const getEnv = (key) => {
30
+ return (config?.get?.(key) ??
31
+ (typeof process !== "undefined" ? process.env[key] : undefined));
32
+ };
33
+ return new NodemailerMailService({
34
+ getService: () => getEnv("MAIL_SERVICE"),
35
+ getHost: () => getEnv("MAIL_HOST"),
36
+ getPort: () => getEnv("MAIL_PORT"),
37
+ getSecure: () => {
38
+ const val = getEnv("MAIL_SECURE");
39
+ return val ? val === "true" || val === "1" : undefined;
40
+ },
41
+ getIgnoreTLS: () => {
42
+ const val = getEnv("MAIL_IGNORE_TLS");
43
+ return val ? val === "true" || val === "1" : undefined;
44
+ },
45
+ getUsername: () => getEnv("GOOGLE_SMTP_APP_USERNAME") ?? getEnv("MAIL_USERNAME"),
46
+ getPassword: () => getEnv("GOOGLE_SMTP_APP_PASSWORD") ?? getEnv("MAIL_PASSWORD"),
47
+ getFromAddress: () => getEnv("MAIL_FROM_ADDRESS"),
48
+ getFromName: () => getEnv("MAIL_FROM_NAME"),
49
+ });
50
+ },
36
51
  inject: [ConfigService],
37
52
  singleton: true,
38
53
  });
39
54
  }
40
55
  async boot() {
41
- // Bind the intl translation factory for transport templates (adapters
42
- // must not import the package's own entry points - ADR-006). It is
43
- // invoked per send, so the locale reflects the current request.
44
- setMailTranslatorFactory(async () => (key) => key);
45
- const config = await this.container.resolve(ConfigService);
56
+ // Bind the default translation factory that supports `{placeholder}`
57
+ // interpolation into the message text.
58
+ setMailTranslatorFactory(async () => (key, params) => {
59
+ if (!params)
60
+ return key;
61
+ return Object.entries(params).reduce((acc, [k, v]) => acc.replaceAll(`{${k}}`, String(v)), key);
62
+ });
63
+ const config = this.container.has(ConfigService)
64
+ ? await this.container.resolve(ConfigService)
65
+ : undefined;
46
66
  const mailer = await this.selectTransport(config);
47
67
  // Rebind the port token to the selected transport and expose it through
48
68
  // the typed context boundary.
@@ -55,7 +75,11 @@ export class CommunicationServiceProvider extends ServiceProvider {
55
75
  logger.debug("veap:communication", `Mail transport bound: ${mailer.constructor.name}`);
56
76
  }
57
77
  async selectTransport(config) {
58
- const transport = config.get("MAIL_TRANSPORT")?.trim().toLowerCase();
78
+ const getVal = (key) => {
79
+ return (config?.get?.(key) ??
80
+ (typeof process !== "undefined" ? process.env[key] : undefined));
81
+ };
82
+ const transport = getVal("MAIL_TRANSPORT")?.trim().toLowerCase();
59
83
  if (transport === "custom") {
60
84
  if (!this.container.has(CUSTOM_MAILER)) {
61
85
  throw new Error("[Communication] MAIL_TRANSPORT=custom but no provider is registered under the CUSTOM_MAILER token (register an IMailer in a ServiceProvider).");
@@ -65,13 +89,14 @@ export class CommunicationServiceProvider extends ServiceProvider {
65
89
  if (transport === "console") {
66
90
  return new ConsoleMailService((line) => logger.info("veap:mail", line));
67
91
  }
68
- const hasSmtpCredentials = Boolean(config.get("GOOGLE_SMTP_APP_USERNAME") || config.get("MAIL_USERNAME")) &&
69
- Boolean(config.get("GOOGLE_SMTP_APP_PASSWORD") || config.get("MAIL_PASSWORD"));
70
- if (!transport && !hasSmtpCredentials) {
71
- logger.warn("veap:communication", "MAIL_TRANSPORT is not set and no SMTP credentials (MAIL_USERNAME/MAIL_PASSWORD) are configured. Defaulting to ConsoleMailService (emails will be logged to server console).");
92
+ const hasSmtpCredentials = Boolean(getVal("GOOGLE_SMTP_APP_USERNAME") || getVal("MAIL_USERNAME")) &&
93
+ Boolean(getVal("GOOGLE_SMTP_APP_PASSWORD") || getVal("MAIL_PASSWORD"));
94
+ const hasSmtpHostOrService = Boolean(getVal("MAIL_HOST") || getVal("MAIL_SERVICE"));
95
+ if (!transport && !hasSmtpCredentials && !hasSmtpHostOrService) {
96
+ logger.warn("veap:communication", "MAIL_TRANSPORT is not set and no SMTP credentials (MAIL_USERNAME/MAIL_PASSWORD) or host (MAIL_HOST) are configured. Defaulting to ConsoleMailService (emails will be logged to server console).");
72
97
  return new ConsoleMailService((line) => logger.info("veap:mail", line));
73
98
  }
74
- // Default ("smtp" / unset with credentials): whatever register() wired - Nodemailer.
99
+ // Default ("smtp" / unset with credentials/host): whatever register() wired - Nodemailer.
75
100
  return this.container.resolve(MAILER);
76
101
  }
77
102
  }
@@ -1,8 +1,7 @@
1
- import type { MailMessage } from ".././mail-message";
2
- import type { IMailer } from ".././mailer";
1
+ import type { MailMessage } from "../mail-message.js";
2
+ import type { IMailer } from "../mailer.js";
3
3
  /**
4
- * Console transport implementing the {@link IMailer} port.
5
- *
4
+ * Console transport implementing the {@link IMailer} port.\n *
6
5
  * A pure delivery service for development and testing: prints the
7
6
  * `MailMessage` instead of dialling an SMTP server. Selected with
8
7
  * `MAIL_TRANSPORT=console`. It knows nothing about message semantics -
@@ -1,6 +1,5 @@
1
1
  /**
2
- * Console transport implementing the {@link IMailer} port.
3
- *
2
+ * Console transport implementing the {@link IMailer} port.\n *
4
3
  * A pure delivery service for development and testing: prints the
5
4
  * `MailMessage` instead of dialling an SMTP server. Selected with
6
5
  * `MAIL_TRANSPORT=console`. It knows nothing about message semantics -
@@ -1,6 +1,17 @@
1
1
  import type { SendMailOptions } from "nodemailer";
2
- import type { MailMessage, MailRecipient } from ".././mail-message";
3
- import type { IMailer } from ".././mailer";
2
+ import type { MailMessage, MailRecipient } from "../mail-message.js";
3
+ import type { IMailer } from "../mailer.js";
4
+ export interface NodemailerMailConfig {
5
+ getService?: () => string | undefined;
6
+ getHost?: () => string | undefined;
7
+ getPort?: () => number | string | undefined;
8
+ getSecure?: () => boolean | undefined;
9
+ getIgnoreTLS?: () => boolean | undefined;
10
+ getUsername?: () => string | undefined;
11
+ getPassword?: () => string | undefined;
12
+ getFromAddress?: () => string | undefined;
13
+ getFromName?: () => string | undefined;
14
+ }
4
15
  /**
5
16
  * SMTP transport (Nodemailer) implementing the {@link IMailer} port.
6
17
  *
@@ -16,12 +27,7 @@ import type { IMailer } from ".././mailer";
16
27
  export declare class NodemailerMailService implements IMailer {
17
28
  private transporter;
18
29
  private fromAddress;
19
- constructor(config: {
20
- getService: () => string;
21
- getUsername: () => string | undefined;
22
- getPassword: () => string | undefined;
23
- getFromAddress: () => string | undefined;
24
- });
30
+ constructor(config: NodemailerMailConfig);
25
31
  /** The sender used when a message does not specify one. */
26
32
  get defaultFrom(): MailRecipient;
27
33
  /** Exposed for tests and provider-agnostic integrations. */
@@ -15,16 +15,59 @@ export class NodemailerMailService {
15
15
  transporter;
16
16
  fromAddress;
17
17
  constructor(config) {
18
- this.fromAddress =
19
- config.getFromAddress() ||
20
- (config.getUsername() ? `Veap <${config.getUsername()}>` : "Veap");
21
- this.transporter = nodemailer.createTransport({
22
- service: config.getService(),
23
- auth: {
24
- user: config.getUsername(),
25
- pass: config.getPassword(),
26
- },
27
- });
18
+ const fromAddress = config.getFromAddress?.();
19
+ const fromName = config.getFromName?.();
20
+ const username = config.getUsername?.();
21
+ const password = config.getPassword?.();
22
+ if (fromAddress) {
23
+ this.fromAddress =
24
+ fromName && !fromAddress.includes("<")
25
+ ? `${fromName} <${fromAddress}>`
26
+ : fromAddress;
27
+ }
28
+ else if (username) {
29
+ this.fromAddress = fromName
30
+ ? `${fromName} <${username}>`
31
+ : `Veap <${username}>`;
32
+ }
33
+ else {
34
+ this.fromAddress = "Veap";
35
+ }
36
+ const host = config.getHost?.();
37
+ const port = config.getPort?.();
38
+ const service = config.getService?.();
39
+ const secure = config.getSecure?.();
40
+ const ignoreTLS = config.getIgnoreTLS?.();
41
+ const auth = username || password
42
+ ? {
43
+ user: username,
44
+ pass: password,
45
+ }
46
+ : undefined;
47
+ if (host) {
48
+ const parsedPort = port ? Number(port) : 587;
49
+ this.transporter = nodemailer.createTransport({
50
+ host,
51
+ port: parsedPort,
52
+ secure: secure !== undefined ? Boolean(secure) : parsedPort === 465,
53
+ ignoreTLS: Boolean(ignoreTLS),
54
+ auth,
55
+ });
56
+ }
57
+ else if (service) {
58
+ this.transporter = nodemailer.createTransport({
59
+ service,
60
+ auth,
61
+ });
62
+ }
63
+ else {
64
+ // Default local or fallback SMTP transport
65
+ this.transporter = nodemailer.createTransport({
66
+ host: "localhost",
67
+ port: 587,
68
+ auth,
69
+ });
70
+ }
28
71
  }
29
72
  /** The sender used when a message does not specify one. */
30
73
  get defaultFrom() {
@@ -7,6 +7,10 @@
7
7
  * directly and the package never imports its own entry points (ADR-006).
8
8
  */
9
9
  export type Translator = (key: string, params?: Record<string, unknown>) => string;
10
+ /**
11
+ * Helper to interpolate template placeholders like `{name}` or `{code}`.
12
+ */
13
+ export declare function interpolateMailText(text: string, params?: Record<string, unknown>): string;
10
14
  /**
11
15
  * Binds the translation factory. Invoked per message build, so the locale
12
16
  * always reflects the current request.
@@ -1,4 +1,12 @@
1
1
  let translatorFactory = null;
2
+ /**
3
+ * Helper to interpolate template placeholders like `{name}` or `{code}`.
4
+ */
5
+ export function interpolateMailText(text, params) {
6
+ if (!params)
7
+ return text;
8
+ return Object.entries(params).reduce((acc, [k, v]) => acc.replaceAll(`{${k}}`, String(v)), text);
9
+ }
2
10
  /**
3
11
  * Binds the translation factory. Invoked per message build, so the locale
4
12
  * always reflects the current request.
@@ -12,9 +20,12 @@ export function setMailTranslatorFactory(factory) {
12
20
  * (unit tests, early boot).
13
21
  */
14
22
  export async function getMailTranslator() {
15
- if (translatorFactory)
16
- return translatorFactory();
17
- return (key, params) => params
18
- ? Object.entries(params).reduce((acc, [k, v]) => acc.replaceAll(`{${k}}`, String(v)), key)
19
- : key;
23
+ if (translatorFactory) {
24
+ const fn = await translatorFactory();
25
+ return (key, params) => {
26
+ const translated = fn(key, params);
27
+ return interpolateMailText(translated, params);
28
+ };
29
+ }
30
+ return (key, params) => interpolateMailText(key, params);
20
31
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veap/mail",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Mail and communication transport abstraction for Veap",
5
5
  "bugs": "https://github.com/veapjs/veap-mail/issues",
6
6
  "repository": {
@@ -28,7 +28,7 @@
28
28
  "test": "vitest run"
29
29
  },
30
30
  "dependencies": {
31
- "@veap/kernel": "0.1.0",
31
+ "@veap/kernel": "0.1.1",
32
32
  "nodemailer": "^10.0.1"
33
33
  },
34
34
  "devDependencies": {