@veap/mail 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +127 -2
- package/dist/composition.d.ts +63 -0
- package/dist/composition.js +8 -0
- package/dist/context.d.ts +22 -0
- package/dist/context.js +20 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +10 -0
- package/dist/mail-message.d.ts +53 -0
- package/dist/mail-message.js +9 -0
- package/dist/mail.d.ts +19 -0
- package/dist/mail.js +52 -0
- package/dist/mailer.d.ts +38 -0
- package/dist/mailer.js +20 -0
- package/dist/provider.d.ts +19 -0
- package/dist/provider.js +103 -0
- package/dist/providers/console.provider.d.ts +14 -0
- package/dist/providers/console.provider.js +33 -0
- package/dist/providers/nodemailer.provider.d.ts +36 -0
- package/dist/providers/nodemailer.provider.js +86 -0
- package/dist/translator.d.ts +24 -0
- package/dist/translator.js +31 -0
- package/package.json +39 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Damian Miller
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,128 @@
|
|
|
1
|
-
|
|
1
|
+
[](https://github.com/veapjs/veap-mail/actions/workflows/CI.yml)
|
|
2
|
+
[](https://github.com/veapjs/veap-mail/actions/workflows/RELEASE.yml)
|
|
3
|
+
[](https://www.npmjs.com/package/@veap/mail)
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
# @veap/mail
|
|
6
|
+
|
|
7
|
+
Modular email and notification transport abstraction with support for
|
|
8
|
+
Nodemailer, SMTP, Console logging, and custom mail drivers for Veap applications.
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
`@veap/mail` provides a unified messaging interface decoupling application
|
|
13
|
+
logic from email delivery protocols. It supports zero-configuration development
|
|
14
|
+
environments with automatic console logging, robust SMTP / Nodemailer delivery
|
|
15
|
+
in production, and a custom driver port (`CUSTOM_MAILER`) for external email
|
|
16
|
+
services such as AWS SES, Postmark, or Resend.
|
|
17
|
+
|
|
18
|
+
## Features
|
|
19
|
+
|
|
20
|
+
- **Multiple mail transports**: Built-in support for Nodemailer (SMTP, Google
|
|
21
|
+
App Passwords) and zero-config `ConsoleMailService` for local development.
|
|
22
|
+
- **Unified `sendMail` facade**: Simple, consistent API for sending plain text,
|
|
23
|
+
HTML, and rich multipart emails.
|
|
24
|
+
- **Authentication notification helpers**: Out-of-the-box functions for
|
|
25
|
+
`sendResetPassword` and `sendVerifyEmail` with customizable templates.
|
|
26
|
+
- **IoC-driven lifecycle**: `MailServiceProvider` handles automatic driver
|
|
27
|
+
selection and credentials parsing during application boot.
|
|
28
|
+
- **Custom mailer support**: Extendable driver architecture via the
|
|
29
|
+
`CUSTOM_MAILER` token.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Using bun
|
|
35
|
+
bun add @veap/mail @veap/kernel
|
|
36
|
+
|
|
37
|
+
# Using pnpm
|
|
38
|
+
pnpm add @veap/mail @veap/kernel
|
|
39
|
+
|
|
40
|
+
# Using npm
|
|
41
|
+
npm install @veap/mail @veap/kernel
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Configuration
|
|
45
|
+
|
|
46
|
+
`@veap/mail` detects transports automatically from environment variables:
|
|
47
|
+
|
|
48
|
+
| Environment Variable | Description |
|
|
49
|
+
| ---------------------------------------- | ---------------------------------------------------- |
|
|
50
|
+
| `MAIL_TRANSPORT` | Delivery driver (`smtp`, `console`, or `custom`) |
|
|
51
|
+
| `MAIL_SERVICE` | Well-known Nodemailer service (for example, `gmail`) |
|
|
52
|
+
| `MAIL_USERNAME` / `MAIL_PASSWORD` | Standard SMTP credentials |
|
|
53
|
+
| `GOOGLE_SMTP_APP_USERNAME` / `_PASSWORD` | Alternative Google App password credentials |
|
|
54
|
+
| `MAIL_FROM_ADDRESS` | Default sender email address |
|
|
55
|
+
|
|
56
|
+
When no credentials or `MAIL_TRANSPORT` are set, `@veap/mail` defaults safely
|
|
57
|
+
to `ConsoleMailService` so local development and tests run without network errors.
|
|
58
|
+
|
|
59
|
+
## Usage
|
|
60
|
+
|
|
61
|
+
### Register in Application Builder
|
|
62
|
+
|
|
63
|
+
Include `MailServiceProvider` in your application composition root:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import { Application } from "@veap/kernel";
|
|
67
|
+
import { MailServiceProvider } from "@veap/mail";
|
|
68
|
+
|
|
69
|
+
export const app = Application.configure().use(MailServiceProvider).create();
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Sending Emails
|
|
73
|
+
|
|
74
|
+
Send transactional messages from services or server actions:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import { sendMail } from "@veap/mail";
|
|
78
|
+
|
|
79
|
+
await sendMail({
|
|
80
|
+
to: "user@example.com",
|
|
81
|
+
subject: "Welcome to Our Platform",
|
|
82
|
+
text: "Thank you for creating an account with us!",
|
|
83
|
+
html: "<h1>Welcome!</h1><p>Thank you for creating an account with us!</p>",
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Registering a Custom Mailer
|
|
88
|
+
|
|
89
|
+
Implement the `IMailer` interface and bind it under `CUSTOM_MAILER`:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import { ServiceProvider } from "@veap/kernel";
|
|
93
|
+
import { CUSTOM_MAILER, type IMailer, type MailMessage } from "@veap/mail";
|
|
94
|
+
|
|
95
|
+
export class ResendMailService implements IMailer {
|
|
96
|
+
async send(message: MailMessage): Promise<void> {
|
|
97
|
+
// Send via third-party SDK (e.g., resend.emails.send)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export class CustomMailServiceProvider extends ServiceProvider {
|
|
102
|
+
register(): void {
|
|
103
|
+
this.container.register({
|
|
104
|
+
token: CUSTOM_MAILER,
|
|
105
|
+
useClass: ResendMailService,
|
|
106
|
+
singleton: true,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Subpath Exports
|
|
113
|
+
|
|
114
|
+
| Export Path | Content |
|
|
115
|
+
| ------------ | ---------------------------------------------------------------------------------------- |
|
|
116
|
+
| `@veap/mail` | Mail facades (`sendMail`), providers, interfaces (`IMailer`, `MailMessage`), and context |
|
|
117
|
+
|
|
118
|
+
## Documentation
|
|
119
|
+
|
|
120
|
+
For full guides and transport configuration options, visit:
|
|
121
|
+
|
|
122
|
+
- **Official Website**: [https://veap.pl](https://veap.pl)
|
|
123
|
+
- **Documentation**: [https://veap.pl/docs](https://veap.pl/docs)
|
|
124
|
+
- **GitHub Repository**: [https://github.com/veapjs/veap-mail](https://github.com/veapjs/veap-mail)
|
|
125
|
+
|
|
126
|
+
## License
|
|
127
|
+
|
|
128
|
+
MIT
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { IMailer } from "./mailer.js";
|
|
2
|
+
/**
|
|
3
|
+
* Typed boundary between the communication use cases and their consumers
|
|
4
|
+
* (facades, application services). The same pattern as
|
|
5
|
+
* `application/auth/context.ts` and `application/plugins/context.ts`.
|
|
6
|
+
*
|
|
7
|
+
* The composition root (`CommunicationServiceProvider`) selects the mail
|
|
8
|
+
* transport once at boot and binds it here.
|
|
9
|
+
*/
|
|
10
|
+
export interface CommunicationContext {
|
|
11
|
+
mailer: IMailer;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Binds the resolved communication services. Called once by the composition root.
|
|
15
|
+
*/
|
|
16
|
+
export declare function bindCommunicationContext(context: CommunicationContext): void;
|
|
17
|
+
/**
|
|
18
|
+
* Returns the bound communication services.
|
|
19
|
+
*
|
|
20
|
+
* @throws If the communication service provider has not booted yet.
|
|
21
|
+
*/
|
|
22
|
+
export declare function communicationContext(): CommunicationContext;
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { AppError } from "@veap/kernel";
|
|
2
|
+
const globalForCommunicationContext = globalThis;
|
|
3
|
+
/**
|
|
4
|
+
* Binds the resolved communication services. Called once by the composition root.
|
|
5
|
+
*/
|
|
6
|
+
export function bindCommunicationContext(context) {
|
|
7
|
+
globalForCommunicationContext.__VEAP_COMMUNICATION_CONTEXT__ = context;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Returns the bound communication services.
|
|
11
|
+
*
|
|
12
|
+
* @throws If the communication service provider has not booted yet.
|
|
13
|
+
*/
|
|
14
|
+
export function communicationContext() {
|
|
15
|
+
const context = globalForCommunicationContext.__VEAP_COMMUNICATION_CONTEXT__;
|
|
16
|
+
if (!context) {
|
|
17
|
+
throw AppError.Internal("[Communication] Context is not bound. CommunicationServiceProvider must boot before the communication facades are used.");
|
|
18
|
+
}
|
|
19
|
+
return context;
|
|
20
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
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";
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport-agnostic mail DTOs.
|
|
3
|
+
*
|
|
4
|
+
* These are the *only* types a mail transport adapter is allowed to require:
|
|
5
|
+
* the port (`IMailer`) speaks this vocabulary, never a concrete provider's
|
|
6
|
+
* (Nodemailer, SES, Postmark, Resend…). Adapters map `MailMessage` onto their
|
|
7
|
+
* own SDK types - that mapping is precisely what makes an adapter an adapter.
|
|
8
|
+
*/
|
|
9
|
+
/** A named email address, e.g. `{ name: "Veap", address: "noreply@app.dev" }`. */
|
|
10
|
+
export interface MailAddress {
|
|
11
|
+
name?: string;
|
|
12
|
+
address: string;
|
|
13
|
+
}
|
|
14
|
+
/** A file attached to the message. */
|
|
15
|
+
export interface MailAttachment {
|
|
16
|
+
filename?: string;
|
|
17
|
+
/** Inline content (string or Buffer). Mutually exclusive with `path`. */
|
|
18
|
+
content?: string | Buffer;
|
|
19
|
+
/** File path to read the attachment from. Mutually exclusive with `content`. */
|
|
20
|
+
path?: string;
|
|
21
|
+
contentType?: string;
|
|
22
|
+
/** Content-ID for inline images referenced from HTML (`cid:`). */
|
|
23
|
+
cid?: string;
|
|
24
|
+
encoding?: string;
|
|
25
|
+
}
|
|
26
|
+
/** An address or a plain string address. */
|
|
27
|
+
export type MailRecipient = string | MailAddress;
|
|
28
|
+
/**
|
|
29
|
+
* The framework's mail message.
|
|
30
|
+
*
|
|
31
|
+
* Intentionally minimal and framework-neutral: a transport adapter maps it
|
|
32
|
+
* onto its provider's native shape. Anything provider-specific (SES
|
|
33
|
+
* `SourceArn`, Postmark `Metadata`, Resend tags…) belongs to adapter
|
|
34
|
+
* configuration, not to this contract.
|
|
35
|
+
*/
|
|
36
|
+
export interface MailMessage {
|
|
37
|
+
/** Primary recipient(s). */
|
|
38
|
+
to: MailRecipient | MailRecipient[];
|
|
39
|
+
subject: string;
|
|
40
|
+
/** Plain-text body. */
|
|
41
|
+
text?: string;
|
|
42
|
+
/** HTML body. At least one of `text` / `html` should be present. */
|
|
43
|
+
html?: string;
|
|
44
|
+
/** Overrides the transport's default sender (e.g. `MAIL_FROM_ADDRESS`). */
|
|
45
|
+
from?: MailRecipient;
|
|
46
|
+
cc?: MailRecipient | MailRecipient[];
|
|
47
|
+
bcc?: MailRecipient | MailRecipient[];
|
|
48
|
+
replyTo?: MailRecipient;
|
|
49
|
+
attachments?: MailAttachment[];
|
|
50
|
+
priority?: "high" | "normal" | "low";
|
|
51
|
+
/** Custom message headers. */
|
|
52
|
+
headers?: Record<string, string>;
|
|
53
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport-agnostic mail DTOs.
|
|
3
|
+
*
|
|
4
|
+
* These are the *only* types a mail transport adapter is allowed to require:
|
|
5
|
+
* the port (`IMailer`) speaks this vocabulary, never a concrete provider's
|
|
6
|
+
* (Nodemailer, SES, Postmark, Resend…). Adapters map `MailMessage` onto their
|
|
7
|
+
* own SDK types - that mapping is precisely what makes an adapter an adapter.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
package/dist/mail.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { MailMessage } from "./mail-message.js";
|
|
2
|
+
/**
|
|
3
|
+
* Mailables - the domain vocabulary of email.
|
|
4
|
+
*
|
|
5
|
+
* Transports deliver; mailables decide *what* is sent. Each helper builds a
|
|
6
|
+
* `MailMessage` (localized via the injected translator) and hands it to the
|
|
7
|
+
* bound `IMailer` port. This is the Laravel split: `Mailable` defines the
|
|
8
|
+
* content, `Transport` only delivers it - a transport never knows whether it
|
|
9
|
+
* is carrying a verification code or a newsletter.
|
|
10
|
+
*
|
|
11
|
+
* Application services (auth flows) and plugins call these helpers; swapping
|
|
12
|
+
* the transport never touches them, and adding a new mail kind never touches
|
|
13
|
+
* a transport.
|
|
14
|
+
*/
|
|
15
|
+
export declare function sendMail(message: MailMessage): Promise<void>;
|
|
16
|
+
export declare function sendVerifyEmail(email: string, code: string): Promise<void>;
|
|
17
|
+
export declare function sendResetPassword(email: string, code: string): Promise<void>;
|
|
18
|
+
export declare function sendRecoveryCode(email: string, recoveryCode: string): Promise<void>;
|
|
19
|
+
export declare function send2FACode(email: string, code: string): Promise<void>;
|
package/dist/mail.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { communicationContext } from "./context.js";
|
|
2
|
+
import { getMailTranslator } from "./translator.js";
|
|
3
|
+
/**
|
|
4
|
+
* Mailables - the domain vocabulary of email.
|
|
5
|
+
*
|
|
6
|
+
* Transports deliver; mailables decide *what* is sent. Each helper builds a
|
|
7
|
+
* `MailMessage` (localized via the injected translator) and hands it to the
|
|
8
|
+
* bound `IMailer` port. This is the Laravel split: `Mailable` defines the
|
|
9
|
+
* content, `Transport` only delivers it - a transport never knows whether it
|
|
10
|
+
* is carrying a verification code or a newsletter.
|
|
11
|
+
*
|
|
12
|
+
* Application services (auth flows) and plugins call these helpers; swapping
|
|
13
|
+
* the transport never touches them, and adding a new mail kind never touches
|
|
14
|
+
* a transport.
|
|
15
|
+
*/
|
|
16
|
+
export async function sendMail(message) {
|
|
17
|
+
return communicationContext().mailer.sendMail(message);
|
|
18
|
+
}
|
|
19
|
+
export async function sendVerifyEmail(email, code) {
|
|
20
|
+
const t = await getMailTranslator();
|
|
21
|
+
return sendMail({
|
|
22
|
+
to: email,
|
|
23
|
+
subject: t("Verify Email"),
|
|
24
|
+
text: t("Verify your email address with your code: {code}", { code }),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
export async function sendResetPassword(email, code) {
|
|
28
|
+
const t = await getMailTranslator();
|
|
29
|
+
return sendMail({
|
|
30
|
+
to: email,
|
|
31
|
+
subject: t("Password Reset"),
|
|
32
|
+
text: t("Verify password reset with code: {code}", { code }),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
export async function sendRecoveryCode(email, recoveryCode) {
|
|
36
|
+
const t = await getMailTranslator();
|
|
37
|
+
return sendMail({
|
|
38
|
+
to: email,
|
|
39
|
+
subject: t("Recovery Code"),
|
|
40
|
+
text: t("Your recovery code is: {recoveryCode}", { recoveryCode }),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
export async function send2FACode(email, code) {
|
|
44
|
+
const t = await getMailTranslator();
|
|
45
|
+
return sendMail({
|
|
46
|
+
to: email,
|
|
47
|
+
subject: t("Your 2FA Code"),
|
|
48
|
+
text: t("Your verification code is: {code}. It will expire in 10 minutes.", {
|
|
49
|
+
code,
|
|
50
|
+
}),
|
|
51
|
+
});
|
|
52
|
+
}
|
package/dist/mailer.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Token } from "@veap/kernel";
|
|
2
|
+
import type { MailMessage } from "./mail-message.js";
|
|
3
|
+
/**
|
|
4
|
+
* Mailer port.
|
|
5
|
+
*
|
|
6
|
+
* A pure delivery service: it transmits `MailMessage`s and knows nothing
|
|
7
|
+
* about *why* an email is being sent. Semantics - verification codes, resets,
|
|
8
|
+
* 2FA, templates, i18n - belong to the domains that own those flows and are
|
|
9
|
+
* expressed as mailables (`application/communication/mail.ts`), which build a
|
|
10
|
+
* `MailMessage` and hand it to this port.
|
|
11
|
+
*
|
|
12
|
+
* This mirrors Laravel's split: `Transport` (the `MailManager`'s drivers)
|
|
13
|
+
* only delivers a raw message, while `Mailable` classes define the content.
|
|
14
|
+
*/
|
|
15
|
+
export interface IMailer {
|
|
16
|
+
/** Delivers a message. The single, transport-agnostic operation. */
|
|
17
|
+
sendMail(message: MailMessage): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Injection token for the mailer port.
|
|
21
|
+
*
|
|
22
|
+
* A global symbol keeps the token stable across HMR / dual-package
|
|
23
|
+
* boundaries, the same reason the container keys class tokens with
|
|
24
|
+
* `Symbol.for(...)`.
|
|
25
|
+
*/
|
|
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>;
|
|
31
|
+
/**
|
|
32
|
+
* Injection token for a custom transport supplied by the host application.
|
|
33
|
+
*
|
|
34
|
+
* Set `MAIL_TRANSPORT=custom` and register an `IMailer` implementation under
|
|
35
|
+
* this token (e.g. SES, Postmark, Resend, an outbox pattern…) to route all
|
|
36
|
+
* mail through your own delivery system without touching the framework.
|
|
37
|
+
*/
|
|
38
|
+
export declare const CUSTOM_MAILER: Token<IMailer>;
|
package/dist/mailer.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Injection token for the mailer port.
|
|
3
|
+
*
|
|
4
|
+
* A global symbol keeps the token stable across HMR / dual-package
|
|
5
|
+
* boundaries, the same reason the container keys class tokens with
|
|
6
|
+
* `Symbol.for(...)`.
|
|
7
|
+
*/
|
|
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;
|
|
13
|
+
/**
|
|
14
|
+
* Injection token for a custom transport supplied by the host application.
|
|
15
|
+
*
|
|
16
|
+
* Set `MAIL_TRANSPORT=custom` and register an `IMailer` implementation under
|
|
17
|
+
* this token (e.g. SES, Postmark, Resend, an outbox pattern…) to route all
|
|
18
|
+
* mail through your own delivery system without touching the framework.
|
|
19
|
+
*/
|
|
20
|
+
export const CUSTOM_MAILER = Symbol.for("veap:communication:custom-mailer");
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ServiceProvider } from "@veap/kernel";
|
|
2
|
+
/**
|
|
3
|
+
* Composition root of the communication context.
|
|
4
|
+
*
|
|
5
|
+
* Selects the mail transport exactly once (in `boot()`) from `MAIL_TRANSPORT`
|
|
6
|
+
* and binds it into `CommunicationContext`, so facades and services never
|
|
7
|
+
* look transports up themselves:
|
|
8
|
+
*
|
|
9
|
+
* - `smtp` (default) - Nodemailer (`MAIL_SERVICE`, `MAIL_HOST`, `MAIL_PORT`,
|
|
10
|
+
* `MAIL_USERNAME`/`MAIL_PASSWORD` or `GOOGLE_SMTP_APP_*`, `MAIL_FROM_ADDRESS`)
|
|
11
|
+
* - `console` - logs the message; dev/test with zero mail configuration
|
|
12
|
+
* - `custom` - resolves `CUSTOM_MAILER`; bring your own SES/Postmark/Resend…
|
|
13
|
+
*/
|
|
14
|
+
export declare class CommunicationServiceProvider extends ServiceProvider {
|
|
15
|
+
register(): void;
|
|
16
|
+
boot(): Promise<void>;
|
|
17
|
+
private selectTransport;
|
|
18
|
+
}
|
|
19
|
+
export declare const MailServiceProvider: typeof CommunicationServiceProvider;
|
package/dist/provider.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { ConfigService, logger, ServiceProvider } from "@veap/kernel";
|
|
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
|
+
/**
|
|
8
|
+
* Composition root of the communication context.
|
|
9
|
+
*
|
|
10
|
+
* Selects the mail transport exactly once (in `boot()`) from `MAIL_TRANSPORT`
|
|
11
|
+
* and binds it into `CommunicationContext`, so facades and services never
|
|
12
|
+
* look transports up themselves:
|
|
13
|
+
*
|
|
14
|
+
* - `smtp` (default) - Nodemailer (`MAIL_SERVICE`, `MAIL_HOST`, `MAIL_PORT`,
|
|
15
|
+
* `MAIL_USERNAME`/`MAIL_PASSWORD` or `GOOGLE_SMTP_APP_*`, `MAIL_FROM_ADDRESS`)
|
|
16
|
+
* - `console` - logs the message; dev/test with zero mail configuration
|
|
17
|
+
* - `custom` - resolves `CUSTOM_MAILER`; bring your own SES/Postmark/Resend…
|
|
18
|
+
*/
|
|
19
|
+
export class CommunicationServiceProvider extends ServiceProvider {
|
|
20
|
+
register() {
|
|
21
|
+
// Default registration: the Nodemailer transport under the port token,
|
|
22
|
+
// with config getters wired here at the composition root (the adapter
|
|
23
|
+
// itself takes plain functions, so it stays container-agnostic).
|
|
24
|
+
// Replaced with a `useValue` binding in `boot()` when MAIL_TRANSPORT
|
|
25
|
+
// selects another transport.
|
|
26
|
+
this.container.register({
|
|
27
|
+
token: MAILER,
|
|
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
|
+
},
|
|
51
|
+
inject: [ConfigService],
|
|
52
|
+
singleton: true,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
async boot() {
|
|
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;
|
|
66
|
+
const mailer = await this.selectTransport(config);
|
|
67
|
+
// Rebind the port token to the selected transport and expose it through
|
|
68
|
+
// the typed context boundary.
|
|
69
|
+
this.container.register({
|
|
70
|
+
token: MAILER,
|
|
71
|
+
useValue: mailer,
|
|
72
|
+
singleton: true,
|
|
73
|
+
});
|
|
74
|
+
bindCommunicationContext({ mailer });
|
|
75
|
+
logger.debug("veap:communication", `Mail transport bound: ${mailer.constructor.name}`);
|
|
76
|
+
}
|
|
77
|
+
async selectTransport(config) {
|
|
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();
|
|
83
|
+
if (transport === "custom") {
|
|
84
|
+
if (!this.container.has(CUSTOM_MAILER)) {
|
|
85
|
+
throw new Error("[Communication] MAIL_TRANSPORT=custom but no provider is registered under the CUSTOM_MAILER token (register an IMailer in a ServiceProvider).");
|
|
86
|
+
}
|
|
87
|
+
return this.container.resolve(CUSTOM_MAILER);
|
|
88
|
+
}
|
|
89
|
+
if (transport === "console") {
|
|
90
|
+
return new ConsoleMailService((line) => logger.info("veap:mail", line));
|
|
91
|
+
}
|
|
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).");
|
|
97
|
+
return new ConsoleMailService((line) => logger.info("veap:mail", line));
|
|
98
|
+
}
|
|
99
|
+
// Default ("smtp" / unset with credentials/host): whatever register() wired - Nodemailer.
|
|
100
|
+
return this.container.resolve(MAILER);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
export const MailServiceProvider = CommunicationServiceProvider;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { MailMessage } from "../mail-message.js";
|
|
2
|
+
import type { IMailer } from "../mailer.js";
|
|
3
|
+
/**
|
|
4
|
+
* Console transport implementing the {@link IMailer} port.\n *
|
|
5
|
+
* A pure delivery service for development and testing: prints the
|
|
6
|
+
* `MailMessage` instead of dialling an SMTP server. Selected with
|
|
7
|
+
* `MAIL_TRANSPORT=console`. It knows nothing about message semantics -
|
|
8
|
+
* content comes fully built from the mailables.
|
|
9
|
+
*/
|
|
10
|
+
export declare class ConsoleMailService implements IMailer {
|
|
11
|
+
private readonly log;
|
|
12
|
+
constructor(log?: (line: string) => void);
|
|
13
|
+
sendMail(message: MailMessage): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Console transport implementing the {@link IMailer} port.\n *
|
|
3
|
+
* A pure delivery service for development and testing: prints the
|
|
4
|
+
* `MailMessage` instead of dialling an SMTP server. Selected with
|
|
5
|
+
* `MAIL_TRANSPORT=console`. It knows nothing about message semantics -
|
|
6
|
+
* content comes fully built from the mailables.
|
|
7
|
+
*/
|
|
8
|
+
export class ConsoleMailService {
|
|
9
|
+
log;
|
|
10
|
+
constructor(log = console.info) {
|
|
11
|
+
this.log = log;
|
|
12
|
+
}
|
|
13
|
+
async sendMail(message) {
|
|
14
|
+
const to = Array.isArray(message.to)
|
|
15
|
+
? message.to
|
|
16
|
+
.map((r) => (typeof r === "string" ? r : r.address))
|
|
17
|
+
.join(", ")
|
|
18
|
+
: typeof message.to === "string"
|
|
19
|
+
? message.to
|
|
20
|
+
: message.to.address;
|
|
21
|
+
this.log([
|
|
22
|
+
`📬 [MAIL:console] to=${to}`,
|
|
23
|
+
`subject="${message.subject}"`,
|
|
24
|
+
message.text ? `text="${message.text}"` : null,
|
|
25
|
+
message.html ? `(html ${message.html.length} bytes)` : null,
|
|
26
|
+
message.attachments?.length
|
|
27
|
+
? `(attachments: ${message.attachments.length})`
|
|
28
|
+
: null,
|
|
29
|
+
]
|
|
30
|
+
.filter(Boolean)
|
|
31
|
+
.join(" "));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { SendMailOptions } from "nodemailer";
|
|
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
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* SMTP transport (Nodemailer) implementing the {@link IMailer} port.
|
|
17
|
+
*
|
|
18
|
+
* A pure delivery service: takes `MailMessage`s and transmits them. It knows
|
|
19
|
+
* nothing about verification codes, resets or i18n - content is decided by
|
|
20
|
+
* the mailables in `application/communication/mail.ts` (the Laravel split:
|
|
21
|
+
* `Mailable` = content, `Transport` = delivery).
|
|
22
|
+
*
|
|
23
|
+
* The only place in the codebase that knows Nodemailer exists; Nodemailer's
|
|
24
|
+
* field names are a superset of the DTO, so mapping is one spread plus a
|
|
25
|
+
* default sender.
|
|
26
|
+
*/
|
|
27
|
+
export declare class NodemailerMailService implements IMailer {
|
|
28
|
+
private transporter;
|
|
29
|
+
private fromAddress;
|
|
30
|
+
constructor(config: NodemailerMailConfig);
|
|
31
|
+
/** The sender used when a message does not specify one. */
|
|
32
|
+
get defaultFrom(): MailRecipient;
|
|
33
|
+
/** Exposed for tests and provider-agnostic integrations. */
|
|
34
|
+
toSendMailOptions(message: MailMessage): SendMailOptions;
|
|
35
|
+
sendMail(message: MailMessage): Promise<void>;
|
|
36
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import nodemailer from "nodemailer";
|
|
2
|
+
/**
|
|
3
|
+
* SMTP transport (Nodemailer) implementing the {@link IMailer} port.
|
|
4
|
+
*
|
|
5
|
+
* A pure delivery service: takes `MailMessage`s and transmits them. It knows
|
|
6
|
+
* nothing about verification codes, resets or i18n - content is decided by
|
|
7
|
+
* the mailables in `application/communication/mail.ts` (the Laravel split:
|
|
8
|
+
* `Mailable` = content, `Transport` = delivery).
|
|
9
|
+
*
|
|
10
|
+
* The only place in the codebase that knows Nodemailer exists; Nodemailer's
|
|
11
|
+
* field names are a superset of the DTO, so mapping is one spread plus a
|
|
12
|
+
* default sender.
|
|
13
|
+
*/
|
|
14
|
+
export class NodemailerMailService {
|
|
15
|
+
transporter;
|
|
16
|
+
fromAddress;
|
|
17
|
+
constructor(config) {
|
|
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
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** The sender used when a message does not specify one. */
|
|
73
|
+
get defaultFrom() {
|
|
74
|
+
return this.fromAddress;
|
|
75
|
+
}
|
|
76
|
+
/** Exposed for tests and provider-agnostic integrations. */
|
|
77
|
+
toSendMailOptions(message) {
|
|
78
|
+
return {
|
|
79
|
+
...message,
|
|
80
|
+
from: message.from ?? this.fromAddress,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
async sendMail(message) {
|
|
84
|
+
await this.transporter.sendMail(this.toSendMailOptions(message));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translation hook for mailables.
|
|
3
|
+
*
|
|
4
|
+
* Lives in the application layer because building localized messages is a
|
|
5
|
+
* use-case concern; the intl implementation (infrastructure) is injected here
|
|
6
|
+
* once by `CommunicationServiceProvider` - so mailables never import intl
|
|
7
|
+
* directly and the package never imports its own entry points (ADR-006).
|
|
8
|
+
*/
|
|
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;
|
|
14
|
+
/**
|
|
15
|
+
* Binds the translation factory. Invoked per message build, so the locale
|
|
16
|
+
* always reflects the current request.
|
|
17
|
+
*/
|
|
18
|
+
export declare function setMailTranslatorFactory(factory: () => Promise<Translator>): void;
|
|
19
|
+
/**
|
|
20
|
+
* Returns the bound translator, falling back to simple `{param}`
|
|
21
|
+
* interpolation into the raw key when intl has not bound one yet
|
|
22
|
+
* (unit tests, early boot).
|
|
23
|
+
*/
|
|
24
|
+
export declare function getMailTranslator(): Promise<Translator>;
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Binds the translation factory. Invoked per message build, so the locale
|
|
12
|
+
* always reflects the current request.
|
|
13
|
+
*/
|
|
14
|
+
export function setMailTranslatorFactory(factory) {
|
|
15
|
+
translatorFactory = factory;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Returns the bound translator, falling back to simple `{param}`
|
|
19
|
+
* interpolation into the raw key when intl has not bound one yet
|
|
20
|
+
* (unit tests, early boot).
|
|
21
|
+
*/
|
|
22
|
+
export async function getMailTranslator() {
|
|
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);
|
|
31
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@veap/mail",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Mail and communication transport abstraction for Veap",
|
|
5
|
+
"bugs": "https://github.com/veapjs/veap-mail/issues",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/veapjs/veap-mail.git"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"main": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./package.json": "./package.json"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"clean": "rimraf dist",
|
|
26
|
+
"build": "rimraf dist && tsc",
|
|
27
|
+
"dev": "tsc --watch",
|
|
28
|
+
"test": "vitest run"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@veap/kernel": "0.1.1",
|
|
32
|
+
"nodemailer": "^10.0.1"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^22.20.2",
|
|
36
|
+
"@types/nodemailer": "^8.0.1",
|
|
37
|
+
"rimraf": "^6.1.3",
|
|
38
|
+
"typescript": "6.0.3",
|
|
39
|
+
"vitest": "^5.0.0"
|
|
40
|
+
}
|
|
41
|
+
}
|