@fluojs/email 1.0.0-beta.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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.ko.md +325 -0
  3. package/README.md +325 -0
  4. package/dist/channel.d.ts +24 -0
  5. package/dist/channel.d.ts.map +1 -0
  6. package/dist/channel.js +64 -0
  7. package/dist/constants.d.ts +6 -0
  8. package/dist/constants.d.ts.map +1 -0
  9. package/dist/constants.js +17 -0
  10. package/dist/errors.d.ts +13 -0
  11. package/dist/errors.d.ts.map +1 -0
  12. package/dist/errors.js +19 -0
  13. package/dist/index.d.ts +9 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +6 -0
  16. package/dist/module.d.ts +45 -0
  17. package/dist/module.d.ts.map +1 -0
  18. package/dist/module.js +151 -0
  19. package/dist/node/node.d.ts +2 -0
  20. package/dist/node/node.d.ts.map +1 -0
  21. package/dist/node/node.js +1 -0
  22. package/dist/node/nodemailer.d.ts +104 -0
  23. package/dist/node/nodemailer.d.ts.map +1 -0
  24. package/dist/node/nodemailer.js +166 -0
  25. package/dist/node.d.ts +2 -0
  26. package/dist/node.d.ts.map +1 -0
  27. package/dist/node.js +1 -0
  28. package/dist/queue-entry.d.ts +4 -0
  29. package/dist/queue-entry.d.ts.map +1 -0
  30. package/dist/queue-entry.js +2 -0
  31. package/dist/queue.d.ts +38 -0
  32. package/dist/queue.d.ts.map +1 -0
  33. package/dist/queue.js +66 -0
  34. package/dist/service.d.ts +81 -0
  35. package/dist/service.d.ts.map +1 -0
  36. package/dist/service.js +275 -0
  37. package/dist/status.d.ts +28 -0
  38. package/dist/status.d.ts.map +1 -0
  39. package/dist/status.js +83 -0
  40. package/dist/tokens.d.ts +10 -0
  41. package/dist/tokens.d.ts.map +1 -0
  42. package/dist/tokens.js +6 -0
  43. package/dist/types.d.ts +242 -0
  44. package/dist/types.d.ts.map +1 -0
  45. package/dist/types.js +1 -0
  46. package/package.json +84 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 fluo contributors
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.ko.md ADDED
@@ -0,0 +1,325 @@
1
+ # @fluojs/email
2
+
3
+ <p><a href="./README.md"><kbd>English</kbd></a> <strong><kbd>한국어</kbd></strong></p>
4
+
5
+ fluo를 위한 transport-agnostic 이메일 코어 패키지입니다. Nest-like 모듈 API, standalone 사용을 위한 주입 가능한 `EmailService`, 그리고 특정 런타임 transport를 내장하지 않는 `@fluojs/notifications` 연동용 1st-party channel/queue adapter를 제공합니다.
6
+
7
+ ## 목차
8
+
9
+ - [설치](#설치)
10
+ - [사용 시점](#사용-시점)
11
+ - [빠른 시작](#빠른-시작)
12
+ - [일반적인 패턴](#일반적인-패턴)
13
+ - [`@fluojs/email/node`를 이용한 Node 전용 SMTP](#fluojs-email-node를-이용한-node-전용-smtp)
14
+ - [`EmailService`를 이용한 standalone 전달](#emailservice를-이용한-standalone-전달)
15
+ - [`@fluojs/notifications`와의 통합](#fluojs-notifications와의-통합)
16
+ - [큐 기반 대량 전달](#큐-기반-대량-전달)
17
+ - [의도적인 제한 사항](#의도적인-제한-사항)
18
+ - [공개 API 개요](#공개-api-개요)
19
+ - [런타임 전용 및 통합 서브패스](#런타임-전용-및-통합-서브패스)
20
+ - [관련 패키지](#관련-패키지)
21
+ - [예제 소스](#예제-소스)
22
+
23
+ ## 설치
24
+
25
+ ```bash
26
+ npm install @fluojs/email
27
+ ```
28
+
29
+ 내장 notifications 채널과 queue worker 연동이 필요할 때만 `@fluojs/notifications`, `@fluojs/queue`를 함께 설치하면 됩니다.
30
+
31
+ ```bash
32
+ npm install @fluojs/notifications @fluojs/queue
33
+ ```
34
+
35
+ 명시적인 `@fluojs/email/node` 서브패스로 Node 전용 SMTP 전달을 사용할 때만 `nodemailer`를 설치하면 됩니다.
36
+
37
+ ```bash
38
+ npm install @fluojs/email nodemailer
39
+ ```
40
+
41
+ Node 전용 SMTP 전달은 이제 명시적인 `@fluojs/email/node` 서브패스에 위치합니다. queue 기반 notifications 통합도 `@fluojs/email/queue` 서브패스로 분리되었고, 이 서브패스용 `@fluojs/queue`는 루트 설치 필수가 아닌 optional peer로 선언됩니다. 루트 `@fluojs/email` 엔트리포인트는 계속 transport-agnostic 상태를 유지하므로 Bun, Deno, Cloudflare, 커스텀 HTTP transport가 Node 전용 또는 queue 전용 동작을 함께 끌어오지 않습니다.
42
+
43
+ ## 사용 시점
44
+
45
+ - 이메일을 직접 보내는 기능과 `@fluojs/notifications` 채널 연동을 한 패키지에서 처리하고 싶을 때.
46
+ - transport 선택을 Node, Bun, Deno, Cloudflare 호환 애플리케이션 경계 전반에서 명시적이고 이식 가능하게 유지해야 할 때.
47
+ - 이메일 transport 리소스가 애플리케이션 bootstrap/shutdown 수명 주기에 참여해야 하지만 코어 패키지가 특정 런타임을 가정하면 안 될 때.
48
+ - 대량 알림 이메일을 요청 경로에서 직접 보내지 않고 `@fluojs/queue`로 넘기고 싶을 때.
49
+
50
+ ## 빠른 시작
51
+
52
+ ### 모듈 등록
53
+
54
+ ```typescript
55
+ import { Module } from '@fluojs/core';
56
+ import { EmailModule, type EmailTransport } from '@fluojs/email';
57
+
58
+ class ExampleTransport implements EmailTransport {
59
+ async send(message) {
60
+ return {
61
+ accepted: message.to.map((entry) => entry.address),
62
+ messageId: crypto.randomUUID(),
63
+ pending: [],
64
+ rejected: [],
65
+ };
66
+ }
67
+ }
68
+
69
+ @Module({
70
+ imports: [
71
+ EmailModule.forRoot({
72
+ defaultFrom: 'noreply@example.com',
73
+ transport: {
74
+ kind: 'example-http-transport',
75
+ create: async () => new ExampleTransport(),
76
+ },
77
+ }),
78
+ ],
79
+ })
80
+ export class AppModule {}
81
+ ```
82
+
83
+ ### 직접 이메일 보내기
84
+
85
+ ```typescript
86
+ import { Inject } from '@fluojs/core';
87
+ import { EmailService } from '@fluojs/email';
88
+
89
+ export class WelcomeService {
90
+ constructor(@Inject(EmailService) private readonly email: EmailService) {}
91
+
92
+ async sendWelcome(address: string) {
93
+ await this.email.send({
94
+ to: [address],
95
+ subject: 'fluo에 오신 것을 환영합니다',
96
+ text: '계정 준비가 완료되었습니다.',
97
+ });
98
+ }
99
+ }
100
+ ```
101
+
102
+ 루트 `@fluojs/email` 공개 표면은 의도적으로 module-first입니다. 이메일 등록은 `EmailModule.forRoot(...)` 또는 `EmailModule.forRootAsync(...)`를 통해 수행해야 합니다.
103
+
104
+ ## 일반적인 패턴
105
+
106
+ ### `@fluojs/email/node`를 이용한 Node 전용 SMTP
107
+
108
+ 런타임 이식 가능한 루트 패키지 계약을 약화시키지 않으면서 1st-party Nodemailer/SMTP 전달이 필요하다면 전용 Node 서브패스를 사용합니다.
109
+
110
+ ```typescript
111
+ import { Module } from '@fluojs/core';
112
+ import { EmailModule } from '@fluojs/email';
113
+ import { createNodemailerEmailTransportFactory } from '@fluojs/email/node';
114
+
115
+ @Module({
116
+ imports: [
117
+ EmailModule.forRoot({
118
+ defaultFrom: 'noreply@example.com',
119
+ transport: createNodemailerEmailTransportFactory({
120
+ smtp: {
121
+ auth: {
122
+ pass: 'smtp-password',
123
+ user: 'smtp-user',
124
+ },
125
+ host: 'smtp.example.com',
126
+ port: 587,
127
+ secure: false,
128
+ },
129
+ }),
130
+ verifyOnModuleInit: true,
131
+ }),
132
+ ],
133
+ })
134
+ export class AppModule {}
135
+ ```
136
+
137
+ Behavioral contract 메모:
138
+
139
+ - `createNodemailerEmailTransportFactory(...)`는 Node 전용이며 `@fluojs/email/node`에서만 export됩니다.
140
+ - 이 factory는 자신이 생성한 Nodemailer transporter 리소스를 소유하므로 `EmailService`가 bootstrap 시 검증하고 shutdown 시 닫을 수 있습니다.
141
+ - `createNodemailerEmailTransport(...)`는 이미 존재하는 Nodemailer transporter를 감싸지만 리소스 소유권은 호출자에게 남깁니다.
142
+ - SMTP 자격 증명은 여전히 명시적인 옵션 또는 DI를 통해 들어와야 합니다. 루트 패키지와 Node 서브패스 모두 `process.env`를 직접 읽지 않습니다.
143
+
144
+ ### `EmailService`를 이용한 standalone 전달
145
+
146
+ notifications foundation을 거치지 않고 직접 이메일 전달을 하고 싶다면 `EmailService`를 사용합니다.
147
+
148
+ ```typescript
149
+ EmailModule.forRootAsync({
150
+ inject: [ConfigService],
151
+ useFactory: (config) => ({
152
+ defaultFrom: config.mail.from,
153
+ transport: {
154
+ kind: config.mail.transportKind,
155
+ create: () => config.mail.transport,
156
+ ownsResources: false,
157
+ },
158
+ }),
159
+ });
160
+ ```
161
+
162
+ Behavioral contract 메모:
163
+
164
+ - `EmailService.send(...)`는 전달 전에 `defaultFrom`과 `defaultReplyTo`를 해석합니다.
165
+ - `EmailService.send(...)`는 `accepted`, `pending`, `rejected` 수신자를 분리해 보존하므로 provider의 부분 실패가 호출자에게 그대로 보입니다.
166
+ - 서비스는 모듈 bootstrap 시 transport를 초기화하고, factory가 소유한 리소스만 애플리케이션 shutdown 시 닫습니다.
167
+ - 이 패키지는 절대로 `process.env`를 직접 읽지 않습니다. 모든 설정은 명시적인 옵션 또는 DI를 통해 들어와야 합니다.
168
+
169
+ ### `@fluojs/notifications`와의 통합
170
+
171
+ `EMAIL_CHANNEL`을 `NotificationsModule.forRootAsync(...)`에 주입하여, 이메일 전용 payload 필드와 template rendering 규칙이 모두 `@fluojs/email` 안에만 남도록 구성합니다.
172
+
173
+ ```typescript
174
+ import { Module } from '@fluojs/core';
175
+ import { EmailModule, EMAIL_CHANNEL } from '@fluojs/email';
176
+ import { NotificationsModule } from '@fluojs/notifications';
177
+
178
+ @Module({
179
+ imports: [
180
+ EmailModule.forRoot({
181
+ defaultFrom: 'noreply@example.com',
182
+ transport: {
183
+ kind: 'transactional-http',
184
+ create: () => transactionalTransport,
185
+ ownsResources: false,
186
+ },
187
+ }),
188
+ NotificationsModule.forRootAsync({
189
+ inject: [EMAIL_CHANNEL],
190
+ useFactory: (channel) => ({
191
+ channels: [channel],
192
+ }),
193
+ }),
194
+ ],
195
+ })
196
+ export class AppModule {}
197
+ ```
198
+
199
+ 지원하는 notification payload 필드:
200
+
201
+ - `to`, `cc`, `bcc`, `from`, `replyTo`
202
+ - `text`, `html`, `attachments`, `headers`
203
+ - 모듈에 renderer가 구성된 경우 `templateData`
204
+
205
+ Behavioral contract 메모:
206
+
207
+ - `EmailChannel`은 `pending` 또는 `rejected` 수신자가 하나라도 있으면 전달을 성공으로 보고하지 않고 notification dispatch를 실패로 처리합니다.
208
+
209
+ ### 큐 기반 대량 전달
210
+
211
+ `@fluojs/notifications`가 대량 이메일 전달을 백그라운드로 넘겨야 한다면 `QueueLifecycleService`를 주입해 `createEmailNotificationsQueueAdapter(queue)`를 만들고 `QueueModule`을 함께 import합니다.
212
+
213
+ ```typescript
214
+ import { Module } from '@fluojs/core';
215
+ import {
216
+ EmailModule,
217
+ EMAIL_CHANNEL,
218
+ } from '@fluojs/email';
219
+ import { createEmailNotificationsQueueAdapter } from '@fluojs/email/queue';
220
+ import { NotificationsModule } from '@fluojs/notifications';
221
+ import { QueueLifecycleService, QueueModule } from '@fluojs/queue';
222
+
223
+ @Module({
224
+ imports: [
225
+ QueueModule.forRoot(),
226
+ EmailModule.forRoot({
227
+ defaultFrom: 'noreply@example.com',
228
+ transport: {
229
+ kind: 'bulk-email-api',
230
+ create: () => bulkEmailTransport,
231
+ ownsResources: false,
232
+ },
233
+ }),
234
+ NotificationsModule.forRootAsync({
235
+ inject: [EMAIL_CHANNEL, QueueLifecycleService],
236
+ useFactory: (channel, queue) => ({
237
+ channels: [channel],
238
+ queue: {
239
+ adapter: createEmailNotificationsQueueAdapter(queue),
240
+ bulkThreshold: 25,
241
+ },
242
+ }),
243
+ }),
244
+ ],
245
+ })
246
+ export class AppModule {}
247
+ ```
248
+
249
+ 내장 queue worker 계약의 기본값은 다음과 같습니다:
250
+
251
+ - `attempts: 3`
252
+ - `backoff: { type: 'exponential', delayMs: 1000 }`
253
+ - `concurrency: 5`
254
+ - `rateLimiter: { max: 50, duration: 1000 }`
255
+ - `jobName: 'fluo.email.notification'`
256
+
257
+ 이 기본값은 `@fluojs/email/queue`에서 `DEFAULT_EMAIL_QUEUE_WORKER_OPTIONS`로 export되므로, 호출 측에서 커스텀 queue adapter/worker를 만들 때 동일한 계약을 문서화하거나 반영할 수 있습니다.
258
+
259
+ ### 의도적인 제한 사항
260
+
261
+ email 패키지는 의도적으로 다음을 **포함하지 않습니다**:
262
+
263
+ - `process.env`에서 transport 자격 증명을 직접 읽는 동작
264
+ - 공유 루트 패키지에 내장된 SMTP 또는 Nodemailer transport 제공
265
+ - `QueueModule` 자동 설정
266
+ - provider 전용 옵션 타입을 `@fluojs/notifications`에 누출하는 것
267
+
268
+ 이 제한 사항은 transport 선택, 템플릿 전략, 큐 도입 여부가 애플리케이션 경계에서 명시적으로 결정되도록 하기 위한 package contract의 일부입니다.
269
+
270
+ ## 공개 API 개요
271
+
272
+ ### 핵심
273
+
274
+ - `EmailModule.forRoot(options)` / `EmailModule.forRootAsync(options)`
275
+ - `EmailService`
276
+ - `EmailChannel`
277
+ - `EMAIL`
278
+ - `EMAIL_CHANNEL`
279
+
280
+ ### 계약과 헬퍼
281
+
282
+ - `EmailMessage`
283
+ - `EmailTransport`
284
+ - `EmailTransportFactory`
285
+ - `EmailTemplateRenderer`
286
+
287
+ ### 통합 서브패스
288
+
289
+ - `@fluojs/email/queue`: `createEmailNotificationsQueueAdapter(queue)`, `DEFAULT_EMAIL_QUEUE_WORKER_OPTIONS`
290
+
291
+ ### 상태 및 에러
292
+
293
+ - `createEmailPlatformStatusSnapshot(...)`
294
+ - `EmailConfigurationError`
295
+ - `EmailMessageValidationError`
296
+
297
+ ### Node 전용 서브패스
298
+
299
+ - `createNodemailerEmailTransport(...)`
300
+ - `createNodemailerEmailTransportFactory(...)`
301
+ - `NodemailerEmailTransport`
302
+
303
+ ## 런타임 전용 및 통합 서브패스
304
+
305
+ | 런타임 | 서브패스 | export |
306
+ | --- | --- | --- |
307
+ | Node.js | `@fluojs/email/node` | `createNodemailerEmailTransport(...)`, `createNodemailerEmailTransportFactory(...)`, `NodemailerEmailTransport` |
308
+
309
+ | 관심사 | 서브패스 | export |
310
+ | --- | --- | --- |
311
+ | queue 기반 notifications 통합 | `@fluojs/email/queue` | `createEmailNotificationsQueueAdapter(queue)`, `DEFAULT_EMAIL_QUEUE_WORKER_OPTIONS` |
312
+
313
+ ## 관련 패키지
314
+
315
+ - `@fluojs/notifications`: `EMAIL_CHANNEL`을 소비하는 공통 오케스트레이션 계층입니다.
316
+ - `@fluojs/queue`: 대량 이메일 전달을 백그라운드에서 처리하려는 경우 권장됩니다.
317
+ - `@fluojs/config`: 환경 직접 접근 없이 transport 자격 증명과 sender 기본값을 해석하려는 경우 권장됩니다.
318
+ - `nodemailer`: `@fluojs/email/node`가 소비하는 Node 전용 SMTP 구현체입니다.
319
+
320
+ ## 예제 소스
321
+
322
+ - `packages/email/src/module.test.ts`: 모듈 등록, 옵션 정규화, async wiring, lifecycle, queue-backed notifications 예제.
323
+ - `packages/email/src/public-surface.test.ts`: 공개 export와 TypeScript 계약 검증 예제.
324
+ - `packages/email/src/node/node.test.ts`: Node 전용 Nodemailer adapter 매핑과 lifecycle 예제.
325
+ - `packages/email/src/status.test.ts`: health/readiness 계약 예제.
package/README.md ADDED
@@ -0,0 +1,325 @@
1
+ # @fluojs/email
2
+
3
+ <p><strong><kbd>English</kbd></strong> <a href="./README.ko.md"><kbd>한국어</kbd></a></p>
4
+
5
+ Transport-agnostic email delivery core for fluo. It provides a Nest-like module API, an injectable `EmailService` for standalone usage, and a first-party channel/queue adapter pair for `@fluojs/notifications` integration without hard-coding any runtime-specific transport.
6
+
7
+ ## Table of Contents
8
+
9
+ - [Installation](#installation)
10
+ - [When to Use](#when-to-use)
11
+ - [Quick Start](#quick-start)
12
+ - [Common Patterns](#common-patterns)
13
+ - [Node-only SMTP with `@fluojs/email/node`](#node-only-smtp-with-fluojs-email-node)
14
+ - [Standalone delivery with `EmailService`](#standalone-delivery-with-emailservice)
15
+ - [Integration with `@fluojs/notifications`](#integration-with-fluojs-notifications)
16
+ - [Queue-backed bulk delivery](#queue-backed-bulk-delivery)
17
+ - [Intentional limitations](#intentional-limitations)
18
+ - [Public API Overview](#public-api-overview)
19
+ - [Runtime-Specific and Integration Subpaths](#runtime-specific-and-integration-subpaths)
20
+ - [Related Packages](#related-packages)
21
+ - [Example Sources](#example-sources)
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ npm install @fluojs/email
27
+ ```
28
+
29
+ Install `@fluojs/notifications` and `@fluojs/queue` only when you want the built-in notifications channel and queue worker integration.
30
+
31
+ ```bash
32
+ npm install @fluojs/notifications @fluojs/queue
33
+ ```
34
+
35
+ Install `nodemailer` only when you use the explicit `@fluojs/email/node` subpath for Node-only SMTP delivery.
36
+
37
+ ```bash
38
+ npm install @fluojs/email nodemailer
39
+ ```
40
+
41
+ Node-specific SMTP delivery is available from the explicit `@fluojs/email/node` subpath. Queue-backed notifications integration is available from `@fluojs/email/queue`, and `@fluojs/queue` is declared as an optional peer for that subpath. The root `@fluojs/email` entrypoint stays transport-agnostic so Bun, Deno, Cloudflare, and custom HTTP transports do not inherit Node-only or queue-specific behavior.
42
+
43
+ ## When to Use
44
+
45
+ - When you want one package that can send email directly and also plug into `@fluojs/notifications`.
46
+ - When transport choice must stay explicit and portable across Node, Bun, Deno, and Cloudflare-compatible application boundaries.
47
+ - When email transport resources must participate in application bootstrap/shutdown without the core package assuming a specific runtime.
48
+ - When bulk notification delivery should enqueue email work through `@fluojs/queue` instead of blocking request paths.
49
+
50
+ ## Quick Start
51
+
52
+ ### Register the module
53
+
54
+ ```typescript
55
+ import { Module } from '@fluojs/core';
56
+ import { EmailModule, type EmailTransport } from '@fluojs/email';
57
+
58
+ class ExampleTransport implements EmailTransport {
59
+ async send(message) {
60
+ return {
61
+ accepted: message.to.map((entry) => entry.address),
62
+ messageId: crypto.randomUUID(),
63
+ pending: [],
64
+ rejected: [],
65
+ };
66
+ }
67
+ }
68
+
69
+ @Module({
70
+ imports: [
71
+ EmailModule.forRoot({
72
+ defaultFrom: 'noreply@example.com',
73
+ transport: {
74
+ kind: 'example-http-transport',
75
+ create: async () => new ExampleTransport(),
76
+ },
77
+ }),
78
+ ],
79
+ })
80
+ export class AppModule {}
81
+ ```
82
+
83
+ ### Send mail directly
84
+
85
+ ```typescript
86
+ import { Inject } from '@fluojs/core';
87
+ import { EmailService } from '@fluojs/email';
88
+
89
+ export class WelcomeService {
90
+ constructor(@Inject(EmailService) private readonly email: EmailService) {}
91
+
92
+ async sendWelcome(address: string) {
93
+ await this.email.send({
94
+ to: [address],
95
+ subject: 'Welcome to fluo',
96
+ text: 'Your account is ready.',
97
+ });
98
+ }
99
+ }
100
+ ```
101
+
102
+ The root `@fluojs/email` surface is intentionally module-first. Register email delivery through `EmailModule.forRoot(...)` or `EmailModule.forRootAsync(...)`.
103
+
104
+ ## Common Patterns
105
+
106
+ ### Node-only SMTP with `@fluojs/email/node`
107
+
108
+ Use the dedicated Node subpath when you want first-party Nodemailer/SMTP delivery without weakening the runtime-portable root package contract.
109
+
110
+ ```typescript
111
+ import { Module } from '@fluojs/core';
112
+ import { EmailModule } from '@fluojs/email';
113
+ import { createNodemailerEmailTransportFactory } from '@fluojs/email/node';
114
+
115
+ @Module({
116
+ imports: [
117
+ EmailModule.forRoot({
118
+ defaultFrom: 'noreply@example.com',
119
+ transport: createNodemailerEmailTransportFactory({
120
+ smtp: {
121
+ auth: {
122
+ pass: 'smtp-password',
123
+ user: 'smtp-user',
124
+ },
125
+ host: 'smtp.example.com',
126
+ port: 587,
127
+ secure: false,
128
+ },
129
+ }),
130
+ verifyOnModuleInit: true,
131
+ }),
132
+ ],
133
+ })
134
+ export class AppModule {}
135
+ ```
136
+
137
+ Behavioral contract notes:
138
+
139
+ - `createNodemailerEmailTransportFactory(...)` is Node-only and is exported exclusively from `@fluojs/email/node`.
140
+ - The factory owns the Nodemailer transporter it creates, so `EmailService` can verify it on bootstrap and close it during shutdown.
141
+ - `createNodemailerEmailTransport(...)` wraps an existing Nodemailer transporter without transferring resource ownership.
142
+ - SMTP credentials still enter through explicit options or DI. Neither the root package nor the Node subpath reads `process.env` directly.
143
+
144
+ ### Standalone delivery with `EmailService`
145
+
146
+ Use `EmailService` when your application wants direct email delivery without going through the notifications foundation.
147
+
148
+ ```typescript
149
+ EmailModule.forRootAsync({
150
+ inject: [ConfigService],
151
+ useFactory: (config) => ({
152
+ defaultFrom: config.mail.from,
153
+ transport: {
154
+ kind: config.mail.transportKind,
155
+ create: () => config.mail.transport,
156
+ ownsResources: false,
157
+ },
158
+ }),
159
+ });
160
+ ```
161
+
162
+ Behavioral contract notes:
163
+
164
+ - `EmailService.send(...)` resolves `defaultFrom` and `defaultReplyTo` before delivery.
165
+ - `EmailService.send(...)` preserves `accepted`, `pending`, and `rejected` recipients separately so partial provider failures stay caller-visible.
166
+ - The service initializes the configured transport during module bootstrap and closes factory-owned resources during application shutdown.
167
+ - The package never reads `process.env` directly. All configuration must enter through explicit options or DI.
168
+
169
+ ### Integration with `@fluojs/notifications`
170
+
171
+ Inject `EMAIL_CHANNEL` into `NotificationsModule.forRootAsync(...)` so the email package remains the only place that understands email-specific payload fields and template rendering.
172
+
173
+ ```typescript
174
+ import { Module } from '@fluojs/core';
175
+ import { EmailModule, EMAIL_CHANNEL } from '@fluojs/email';
176
+ import { NotificationsModule } from '@fluojs/notifications';
177
+
178
+ @Module({
179
+ imports: [
180
+ EmailModule.forRoot({
181
+ defaultFrom: 'noreply@example.com',
182
+ transport: {
183
+ kind: 'transactional-http',
184
+ create: () => transactionalTransport,
185
+ ownsResources: false,
186
+ },
187
+ }),
188
+ NotificationsModule.forRootAsync({
189
+ inject: [EMAIL_CHANNEL],
190
+ useFactory: (channel) => ({
191
+ channels: [channel],
192
+ }),
193
+ }),
194
+ ],
195
+ })
196
+ export class AppModule {}
197
+ ```
198
+
199
+ Supported notification payload fields:
200
+
201
+ - `to`, `cc`, `bcc`, `from`, `replyTo`
202
+ - `text`, `html`, `attachments`, `headers`
203
+ - `templateData` when a renderer is configured on the module
204
+
205
+ Behavioral contract notes:
206
+
207
+ - `EmailChannel` treats any `pending` or `rejected` recipients as a failed notification dispatch instead of reporting the delivery as successful.
208
+
209
+ ### Queue-backed bulk delivery
210
+
211
+ When `@fluojs/notifications` should offload bulk email delivery to the background, inject `QueueLifecycleService`, call `createEmailNotificationsQueueAdapter(queue)`, and import `QueueModule`.
212
+
213
+ ```typescript
214
+ import { Module } from '@fluojs/core';
215
+ import {
216
+ EmailModule,
217
+ EMAIL_CHANNEL,
218
+ } from '@fluojs/email';
219
+ import { createEmailNotificationsQueueAdapter } from '@fluojs/email/queue';
220
+ import { NotificationsModule } from '@fluojs/notifications';
221
+ import { QueueLifecycleService, QueueModule } from '@fluojs/queue';
222
+
223
+ @Module({
224
+ imports: [
225
+ QueueModule.forRoot(),
226
+ EmailModule.forRoot({
227
+ defaultFrom: 'noreply@example.com',
228
+ transport: {
229
+ kind: 'bulk-email-api',
230
+ create: () => bulkEmailTransport,
231
+ ownsResources: false,
232
+ },
233
+ }),
234
+ NotificationsModule.forRootAsync({
235
+ inject: [EMAIL_CHANNEL, QueueLifecycleService],
236
+ useFactory: (channel, queue) => ({
237
+ channels: [channel],
238
+ queue: {
239
+ adapter: createEmailNotificationsQueueAdapter(queue),
240
+ bulkThreshold: 25,
241
+ },
242
+ }),
243
+ }),
244
+ ],
245
+ })
246
+ export class AppModule {}
247
+ ```
248
+
249
+ The built-in queue worker contract uses these defaults:
250
+
251
+ - `attempts: 3`
252
+ - `backoff: { type: 'exponential', delayMs: 1000 }`
253
+ - `concurrency: 5`
254
+ - `rateLimiter: { max: 50, duration: 1000 }`
255
+ - `jobName: 'fluo.email.notification'`
256
+
257
+ These defaults are exported from `@fluojs/email/queue` as `DEFAULT_EMAIL_QUEUE_WORKER_OPTIONS` so callers can document or mirror them when they build custom queue adapters/workers.
258
+
259
+ ### Intentional limitations
260
+
261
+ The email package intentionally does **not**:
262
+
263
+ - read transport credentials from `process.env`
264
+ - ship a built-in SMTP or Nodemailer transport in the shared root package
265
+ - configure `QueueModule` automatically
266
+ - leak provider-specific option types into `@fluojs/notifications`
267
+
268
+ These limitations are part of the package contract so transport selection, template strategy, and queue rollout stay explicit at the application boundary.
269
+
270
+ ## Public API Overview
271
+
272
+ ### Core
273
+
274
+ - `EmailModule.forRoot(options)` / `EmailModule.forRootAsync(options)`
275
+ - `EmailService`
276
+ - `EmailChannel`
277
+ - `EMAIL`
278
+ - `EMAIL_CHANNEL`
279
+
280
+ ### Contracts and helpers
281
+
282
+ - `EmailMessage`
283
+ - `EmailTransport`
284
+ - `EmailTransportFactory`
285
+ - `EmailTemplateRenderer`
286
+
287
+ ### Integration subpaths
288
+
289
+ - `@fluojs/email/queue`: `createEmailNotificationsQueueAdapter(queue)`, `DEFAULT_EMAIL_QUEUE_WORKER_OPTIONS`
290
+
291
+ ### Status and errors
292
+
293
+ - `createEmailPlatformStatusSnapshot(...)`
294
+ - `EmailConfigurationError`
295
+ - `EmailMessageValidationError`
296
+
297
+ ### Node-only subpath
298
+
299
+ - `createNodemailerEmailTransport(...)`
300
+ - `createNodemailerEmailTransportFactory(...)`
301
+ - `NodemailerEmailTransport`
302
+
303
+ ## Runtime-Specific and Integration Subpaths
304
+
305
+ | Runtime | Subpath | Exports |
306
+ | --- | --- | --- |
307
+ | Node.js | `@fluojs/email/node` | `createNodemailerEmailTransport(...)`, `createNodemailerEmailTransportFactory(...)`, `NodemailerEmailTransport` |
308
+
309
+ | Concern | Subpath | Exports |
310
+ | --- | --- | --- |
311
+ | Queue-backed notifications integration | `@fluojs/email/queue` | `createEmailNotificationsQueueAdapter(queue)`, `DEFAULT_EMAIL_QUEUE_WORKER_OPTIONS` |
312
+
313
+ ## Related Packages
314
+
315
+ - `@fluojs/notifications`: Shared orchestration layer that consumes `EMAIL_CHANNEL`.
316
+ - `@fluojs/queue`: Recommended when bulk email delivery should run in the background.
317
+ - `@fluojs/config`: Recommended for resolving transport credentials and sender defaults without direct environment access.
318
+ - `nodemailer`: The Node-only SMTP implementation consumed by `@fluojs/email/node`.
319
+
320
+ ## Example Sources
321
+
322
+ - `packages/email/src/module.test.ts`: Module registration, option normalization, async wiring, lifecycle, and queue-backed notifications examples.
323
+ - `packages/email/src/public-surface.test.ts`: Public export and TypeScript contract verification.
324
+ - `packages/email/src/node/node.test.ts`: Node-only Nodemailer adapter mapping and lifecycle examples.
325
+ - `packages/email/src/status.test.ts`: Health/readiness contract examples.
@@ -0,0 +1,24 @@
1
+ import type { NotificationChannel, NotificationChannelDelivery, NotificationChannelContext } from '@fluojs/notifications';
2
+ import { EmailService } from './service.js';
3
+ import type { EmailNotificationDispatchRequest, EmailSendResult, NormalizedEmailModuleOptions } from './types.js';
4
+ /**
5
+ * Notification channel implementation that bridges `@fluojs/notifications` to {@link EmailService}.
6
+ *
7
+ * @remarks
8
+ * This class keeps the foundation package channel-agnostic while allowing `@fluojs/email`
9
+ * to interpret email-specific payload fields, template rendering, and transport delivery.
10
+ */
11
+ export declare class EmailChannel implements NotificationChannel<EmailNotificationDispatchRequest, EmailSendResult> {
12
+ private readonly email;
13
+ readonly channel: string;
14
+ constructor(email: EmailService, options: NormalizedEmailModuleOptions);
15
+ /**
16
+ * Sends one notifications foundation request through the configured email transport.
17
+ *
18
+ * @param notification Shared notification envelope understood by the email package.
19
+ * @param context Optional abort context propagated from the notifications service.
20
+ * @returns A normalized channel delivery result with the provider message id exposed as `externalId`.
21
+ */
22
+ send(notification: EmailNotificationDispatchRequest, context: NotificationChannelContext): Promise<NotificationChannelDelivery<EmailSendResult>>;
23
+ }
24
+ //# sourceMappingURL=channel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,2BAA2B,EAAE,0BAA0B,EAAE,MAAM,uBAAuB,CAAC;AAE1H,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE5C,OAAO,KAAK,EAAE,gCAAgC,EAAE,eAAe,EAAE,4BAA4B,EAAE,MAAM,YAAY,CAAC;AAUlH;;;;;;GAMG;AACH,qBACa,YAAa,YAAW,mBAAmB,CAAC,gCAAgC,EAAE,eAAe,CAAC;IAIvG,OAAO,CAAC,QAAQ,CAAC,KAAK;IAHxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;gBAGN,KAAK,EAAE,YAAY,EACpC,OAAO,EAAE,4BAA4B;IAKvC;;;;;;OAMG;IACG,IAAI,CACR,YAAY,EAAE,gCAAgC,EAC9C,OAAO,EAAE,0BAA0B,GAClC,OAAO,CAAC,2BAA2B,CAAC,eAAe,CAAC,CAAC;CAmBzD"}