@fluojs/email 1.0.0-beta.3 → 1.0.0-beta.5
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.ko.md +11 -2
- package/README.md +11 -2
- package/dist/errors.d.ts +6 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +10 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +4 -1
- package/dist/node/nodemailer.d.ts.map +1 -1
- package/dist/node/nodemailer.js +15 -1
- package/dist/service.d.ts +6 -0
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +99 -11
- package/package.json +7 -7
package/README.ko.md
CHANGED
|
@@ -86,8 +86,9 @@ export class AppModule {}
|
|
|
86
86
|
import { Inject } from '@fluojs/core';
|
|
87
87
|
import { EmailService } from '@fluojs/email';
|
|
88
88
|
|
|
89
|
+
@Inject(EmailService)
|
|
89
90
|
export class WelcomeService {
|
|
90
|
-
constructor(
|
|
91
|
+
constructor(private readonly email: EmailService) {}
|
|
91
92
|
|
|
92
93
|
async sendWelcome(address: string) {
|
|
93
94
|
await this.email.send({
|
|
@@ -139,6 +140,7 @@ Behavioral contract 메모:
|
|
|
139
140
|
- `createNodemailerEmailTransportFactory(...)`는 Node 전용이며 `@fluojs/email/node`에서만 export됩니다.
|
|
140
141
|
- 이 factory는 자신이 생성한 Nodemailer transporter 리소스를 소유하므로 `EmailService`가 bootstrap 시 검증하고 shutdown 시 닫을 수 있습니다.
|
|
141
142
|
- `createNodemailerEmailTransport(...)`는 이미 존재하는 Nodemailer transporter를 감싸지만 리소스 소유권은 호출자에게 남깁니다.
|
|
143
|
+
- Nodemailer display-name 주소는 구조화된 address object로 전달되며, newline 문자가 포함되면 provider handoff 전에 거부됩니다.
|
|
142
144
|
- SMTP 자격 증명은 여전히 명시적인 옵션 또는 DI를 통해 들어와야 합니다. 루트 패키지와 Node 서브패스 모두 `process.env`를 직접 읽지 않습니다.
|
|
143
145
|
|
|
144
146
|
### `EmailService`를 이용한 standalone 전달
|
|
@@ -162,10 +164,15 @@ EmailModule.forRootAsync({
|
|
|
162
164
|
Behavioral contract 메모:
|
|
163
165
|
|
|
164
166
|
- `EmailService.send(...)`는 전달 전에 `defaultFrom`과 `defaultReplyTo`를 해석합니다.
|
|
167
|
+
- `EmailService.send(...)`는 빈 `to` 수신자를 transport handoff 전에 거부하므로 transport가 빈 전달 대상을 받지 않습니다.
|
|
168
|
+
- `EmailService.send(...)`와 `EmailService.sendNotification(...)`은 이미 abort된 `AbortSignal`을 템플릿 렌더링 또는 transport handoff 전에 반영합니다.
|
|
165
169
|
- `EmailService.send(...)`는 `accepted`, `pending`, `rejected` 수신자를 분리해 보존하므로 provider의 부분 실패가 호출자에게 그대로 보입니다.
|
|
166
170
|
- `EmailService.sendMany(...)`는 기본적으로 fail-fast입니다. 실패를 batch result에 수집하려면 `continueOnError: true`를 전달합니다.
|
|
167
171
|
- `EmailService.createPlatformStatusSnapshot()`은 diagnostics를 위해 lifecycle, readiness, health, transport ownership details를 노출합니다.
|
|
168
|
-
- 서비스는 모듈 bootstrap 시 transport를
|
|
172
|
+
- 서비스는 모듈 bootstrap 시 transport를 초기화하며, `verifyOnModuleInit: true`인 경우 bootstrap 검증이 성공적으로 끝날 때까지 delivery가 transport handoff로 진행되지 않습니다.
|
|
173
|
+
- 거부된 `forRootAsync(...)` 옵션 factory 결과는 영구 memoize되지 않으며, 다음 provider resolution에서 configuration lookup을 다시 시도할 수 있습니다.
|
|
174
|
+
- shutdown이 시작된 뒤에는 `EmailService.send(...)`와 `EmailService.sendNotification(...)`이 transport를 재사용하거나 lazy 생성하지 않고 `EmailLifecycleError`로 실패합니다. 진행 중인 factory 소유 transport 생성은 shutdown이 기다린 뒤 닫습니다.
|
|
175
|
+
- transport `verify()`와 `close()`에서 발생한 provider error는 diagnostics를 위해 lifecycle failure의 `cause`로 보존됩니다.
|
|
169
176
|
- 모듈 옵션은 provider wiring 전에 trim 및 normalize됩니다. 여기에는 sender 기본값, notification channel 이름, transport factory 소유권이 포함됩니다.
|
|
170
177
|
- 이 패키지는 절대로 `process.env`를 직접 읽지 않습니다. 모든 설정은 명시적인 옵션 또는 DI를 통해 들어와야 합니다.
|
|
171
178
|
|
|
@@ -209,6 +216,7 @@ Behavioral contract 메모:
|
|
|
209
216
|
|
|
210
217
|
- `EmailChannel`은 `pending` 또는 `rejected` 수신자가 하나라도 있으면 전달을 성공으로 보고하지 않고 notification dispatch를 실패로 처리합니다.
|
|
211
218
|
- `EmailService.sendNotification(...)`은 렌더링된 template output을 payload 및 notification metadata와 병합합니다. payload 필드는 notification fallback보다 우선합니다.
|
|
219
|
+
- Template rendering에는 notification `payload`, `metadata`, `locale`, `subject`, `template`이 전달되며, payload `text`, `html`과 notification `subject`가 렌더링된 fallback보다 우선합니다.
|
|
212
220
|
|
|
213
221
|
### 큐 기반 대량 전달
|
|
214
222
|
|
|
@@ -306,6 +314,7 @@ email 패키지는 의도적으로 다음을 **포함하지 않습니다**:
|
|
|
306
314
|
|
|
307
315
|
- `createEmailPlatformStatusSnapshot(...)`
|
|
308
316
|
- `EmailConfigurationError`
|
|
317
|
+
- `EmailLifecycleError`: lifecycle로 차단된 전달, transport 초기화 또는 검증, 소유 리소스 shutdown 실패에서 발생합니다. 애플리케이션 teardown과 전송이 경합할 수 있다면 이 에러를 catch하세요.
|
|
309
318
|
- `EmailMessageValidationError`
|
|
310
319
|
|
|
311
320
|
### Node 전용 서브패스
|
package/README.md
CHANGED
|
@@ -86,8 +86,9 @@ export class AppModule {}
|
|
|
86
86
|
import { Inject } from '@fluojs/core';
|
|
87
87
|
import { EmailService } from '@fluojs/email';
|
|
88
88
|
|
|
89
|
+
@Inject(EmailService)
|
|
89
90
|
export class WelcomeService {
|
|
90
|
-
constructor(
|
|
91
|
+
constructor(private readonly email: EmailService) {}
|
|
91
92
|
|
|
92
93
|
async sendWelcome(address: string) {
|
|
93
94
|
await this.email.send({
|
|
@@ -139,6 +140,7 @@ Behavioral contract notes:
|
|
|
139
140
|
- `createNodemailerEmailTransportFactory(...)` is Node-only and is exported exclusively from `@fluojs/email/node`.
|
|
140
141
|
- The factory owns the Nodemailer transporter it creates, so `EmailService` can verify it on bootstrap and close it during shutdown.
|
|
141
142
|
- `createNodemailerEmailTransport(...)` wraps an existing Nodemailer transporter without transferring resource ownership.
|
|
143
|
+
- Nodemailer display-name addresses are forwarded as structured address objects and reject newline characters before provider handoff.
|
|
142
144
|
- SMTP credentials still enter through explicit options or DI. Neither the root package nor the Node subpath reads `process.env` directly.
|
|
143
145
|
|
|
144
146
|
### Standalone delivery with `EmailService`
|
|
@@ -162,10 +164,15 @@ EmailModule.forRootAsync({
|
|
|
162
164
|
Behavioral contract notes:
|
|
163
165
|
|
|
164
166
|
- `EmailService.send(...)` resolves `defaultFrom` and `defaultReplyTo` before delivery.
|
|
167
|
+
- `EmailService.send(...)` rejects blank `to` recipients before handoff so transports never receive an empty delivery target.
|
|
168
|
+
- `EmailService.send(...)` and `EmailService.sendNotification(...)` honor an already-aborted `AbortSignal` before template rendering or transport handoff.
|
|
165
169
|
- `EmailService.send(...)` preserves `accepted`, `pending`, and `rejected` recipients separately so partial provider failures stay caller-visible.
|
|
166
170
|
- `EmailService.sendMany(...)` is fail-fast by default; pass `continueOnError: true` to collect failures in a batch result.
|
|
167
171
|
- `EmailService.createPlatformStatusSnapshot()` exposes lifecycle, readiness, health, and transport ownership details for diagnostics.
|
|
168
|
-
- The service initializes the configured transport during module bootstrap and
|
|
172
|
+
- The service initializes the configured transport during module bootstrap and, when `verifyOnModuleInit: true`, delivery waits until bootstrap verification has completed successfully before transport handoff.
|
|
173
|
+
- Rejected `forRootAsync(...)` option factories are not memoized permanently; the next provider resolution can retry configuration lookup.
|
|
174
|
+
- Once shutdown starts, `EmailService.send(...)` and `EmailService.sendNotification(...)` fail with `EmailLifecycleError` instead of reusing or lazily creating transports; any in-flight factory-owned transport creation is awaited and closed by shutdown.
|
|
175
|
+
- Transport `verify()` and `close()` provider errors are preserved as the `cause` of lifecycle failures for diagnostics.
|
|
169
176
|
- Module options are trimmed and normalized before provider wiring, including sender defaults, notification channel names, and transport factory ownership.
|
|
170
177
|
- The package never reads `process.env` directly. All configuration must enter through explicit options or DI.
|
|
171
178
|
|
|
@@ -209,6 +216,7 @@ Behavioral contract notes:
|
|
|
209
216
|
|
|
210
217
|
- `EmailChannel` treats any `pending` or `rejected` recipients as a failed notification dispatch instead of reporting the delivery as successful.
|
|
211
218
|
- `EmailService.sendNotification(...)` merges rendered template output with payload and notification metadata; payload fields override notification fallbacks.
|
|
219
|
+
- Template rendering receives notification `payload`, `metadata`, `locale`, `subject`, and `template`; payload `text`, `html`, and notification `subject` override rendered fallbacks.
|
|
212
220
|
|
|
213
221
|
### Queue-backed bulk delivery
|
|
214
222
|
|
|
@@ -306,6 +314,7 @@ These limitations are part of the package contract so transport selection, templ
|
|
|
306
314
|
|
|
307
315
|
- `createEmailPlatformStatusSnapshot(...)`
|
|
308
316
|
- `EmailConfigurationError`
|
|
317
|
+
- `EmailLifecycleError`: thrown by lifecycle-gated delivery, transport initialization or verification, and owned-resource shutdown failures. Catch this error when sends can race with application teardown.
|
|
309
318
|
- `EmailMessageValidationError`
|
|
310
319
|
|
|
311
320
|
### Node-only subpath
|
package/dist/errors.d.ts
CHANGED
|
@@ -10,4 +10,10 @@ export declare class EmailConfigurationError extends Error {
|
|
|
10
10
|
export declare class EmailMessageValidationError extends Error {
|
|
11
11
|
constructor(message: string);
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* Thrown when email delivery is requested after the service lifecycle has started shutting down.
|
|
15
|
+
*/
|
|
16
|
+
export declare class EmailLifecycleError extends Error {
|
|
17
|
+
constructor(message: string, options?: ErrorOptions);
|
|
18
|
+
}
|
|
13
19
|
//# sourceMappingURL=errors.d.ts.map
|
package/dist/errors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;gBACpC,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,qBAAa,2BAA4B,SAAQ,KAAK;gBACxC,OAAO,EAAE,MAAM;CAI5B"}
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;gBACpC,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,qBAAa,2BAA4B,SAAQ,KAAK;gBACxC,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY;CAIpD"}
|
package/dist/errors.js
CHANGED
|
@@ -16,4 +16,14 @@ export class EmailMessageValidationError extends Error {
|
|
|
16
16
|
super(message);
|
|
17
17
|
this.name = 'EmailMessageValidationError';
|
|
18
18
|
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Thrown when email delivery is requested after the service lifecycle has started shutting down.
|
|
23
|
+
*/
|
|
24
|
+
export class EmailLifecycleError extends Error {
|
|
25
|
+
constructor(message, options) {
|
|
26
|
+
super(message, options);
|
|
27
|
+
this.name = 'EmailLifecycleError';
|
|
28
|
+
}
|
|
19
29
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { EmailConfigurationError, EmailMessageValidationError, } from './errors.js';
|
|
1
|
+
export { EmailConfigurationError, EmailLifecycleError, EmailMessageValidationError, } from './errors.js';
|
|
2
2
|
export { EmailChannel } from './channel.js';
|
|
3
3
|
export { EmailModule } from './module.js';
|
|
4
4
|
export { EmailService } from './service.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,uBAAuB,EACvB,2BAA2B,GAC5B,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,iCAAiC,EAAE,MAAM,aAAa,CAAC;AAChE,YAAY,EAAE,mBAAmB,EAAE,2BAA2B,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAC7G,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACnD,YAAY,EACV,KAAK,EACL,YAAY,EACZ,gBAAgB,EAChB,uBAAuB,EACvB,eAAe,EACf,YAAY,EACZ,kBAAkB,EAClB,gCAAgC,EAChC,wBAAwB,EACxB,oBAAoB,EACpB,gBAAgB,EAChB,oBAAoB,EACpB,gBAAgB,EAChB,eAAe,EACf,0BAA0B,EAC1B,sBAAsB,EACtB,wBAAwB,EACxB,qBAAqB,EACrB,yBAAyB,EACzB,cAAc,EACd,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,uBAAuB,EACvB,mBAAmB,EACnB,2BAA2B,GAC5B,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,iCAAiC,EAAE,MAAM,aAAa,CAAC;AAChE,YAAY,EAAE,mBAAmB,EAAE,2BAA2B,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAC7G,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACnD,YAAY,EACV,KAAK,EACL,YAAY,EACZ,gBAAgB,EAChB,uBAAuB,EACvB,eAAe,EACf,YAAY,EACZ,kBAAkB,EAClB,gCAAgC,EAChC,wBAAwB,EACxB,oBAAoB,EACpB,gBAAgB,EAChB,oBAAoB,EACpB,gBAAgB,EAChB,eAAe,EACf,0BAA0B,EAC1B,sBAAsB,EACtB,wBAAwB,EACxB,qBAAqB,EACrB,yBAAyB,EACzB,cAAc,EACd,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { EmailConfigurationError, EmailMessageValidationError } from './errors.js';
|
|
1
|
+
export { EmailConfigurationError, EmailLifecycleError, EmailMessageValidationError } from './errors.js';
|
|
2
2
|
export { EmailChannel } from './channel.js';
|
|
3
3
|
export { EmailModule } from './module.js';
|
|
4
4
|
export { EmailService } from './service.js';
|
package/dist/module.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAMhE,OAAO,KAAK,EAGV,uBAAuB,EAGvB,kBAAkB,EAEnB,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAMhE,OAAO,KAAK,EAGV,uBAAuB,EAGvB,kBAAkB,EAEnB,MAAM,YAAY,CAAC;AA8HpB,kFAAkF;AAClF,qBAAa,WAAW;IACtB;;;;;;;;;;;;;;;;QAgBI;IACJ,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,kBAAkB,GAAG,UAAU;IAIvD;;;;;;;;;;;;;;;;;;;OAmBG;IACH,MAAM,CAAC,YAAY,CAAC,OAAO,EAAE,uBAAuB,GAAG,UAAU;CAGlE"}
|
package/dist/module.js
CHANGED
|
@@ -85,7 +85,10 @@ function buildEmailModuleAsync(options) {
|
|
|
85
85
|
let cachedResult;
|
|
86
86
|
const memoizedFactory = (...deps) => {
|
|
87
87
|
if (!cachedResult) {
|
|
88
|
-
cachedResult = Promise.resolve(factory(...deps)).then(resolved => normalizeEmailModuleOptions(resolved))
|
|
88
|
+
cachedResult = Promise.resolve(factory(...deps)).then(resolved => normalizeEmailModuleOptions(resolved)).catch(error => {
|
|
89
|
+
cachedResult = undefined;
|
|
90
|
+
throw error;
|
|
91
|
+
});
|
|
89
92
|
}
|
|
90
93
|
return cachedResult;
|
|
91
94
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nodemailer.d.ts","sourceRoot":"","sources":["../../src/node/nodemailer.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,IAAI,MAAM,uBAAuB,CAAC;AAC9C,OAAO,KAAK,aAAa,MAAM,+BAA+B,CAAC;AAE/D,OAAO,KAAK,EAEV,cAAc,EACd,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,sBAAsB,EACvB,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"nodemailer.d.ts","sourceRoot":"","sources":["../../src/node/nodemailer.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,IAAI,MAAM,uBAAuB,CAAC;AAC9C,OAAO,KAAK,aAAa,MAAM,+BAA+B,CAAC;AAE/D,OAAO,KAAK,EAEV,cAAc,EACd,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,sBAAsB,EACvB,MAAM,aAAa,CAAC;AAGrB,4FAA4F;AAC5F,MAAM,MAAM,qBAAqB,GAAG,IAAI,CAAC,aAAa,CAAC,eAAe,CAAC,CAAC;AAExE;;;;;;;GAOG;AACH,MAAM,WAAW,+BAA+B;IAC9C,iFAAiF;IACjF,WAAW,EAAE,qBAAqB,CAAC;CACpC;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,sCAAsC;IACrD,iFAAiF;IACjF,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd,iGAAiG;IACjG,IAAI,EAAE,aAAa,CAAC,OAAO,GAAG,MAAM,CAAC;CACtC;AAiED;;;;;;GAMG;AACH,qBAAa,wBAAyB,YAAW,cAAc;IACjD,OAAO,CAAC,QAAQ,CAAC,WAAW;gBAAX,WAAW,EAAE,qBAAqB;IAE/D;;;;;;;;;;;;OAYG;IACG,IAAI,CAAC,OAAO,EAAE,sBAAsB,EAAE,QAAQ,EAAE,qBAAqB,GAAG,OAAO,CAAC,qBAAqB,CAAC;IAa5G;;;;OAIG;IACG,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAI7B;;;;OAIG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,+BAA+B,GAAG,cAAc,CAEvG;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,qCAAqC,CACnD,OAAO,EAAE,sCAAsC,GAC9C,qBAAqB,CAQvB"}
|
package/dist/node/nodemailer.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
2
|
import nodemailer from 'nodemailer';
|
|
3
|
+
import { EmailMessageValidationError } from '../errors.js';
|
|
3
4
|
|
|
4
5
|
/** Node-only Nodemailer transporter type used by the explicit `@fluojs/email/node` seam. */
|
|
5
6
|
|
|
@@ -21,8 +22,21 @@ import nodemailer from 'nodemailer';
|
|
|
21
22
|
* isolated to the Node-only subpath.
|
|
22
23
|
*/
|
|
23
24
|
|
|
25
|
+
function assertSafeAddressPart(value, label) {
|
|
26
|
+
if (/\r|\n/.test(value)) {
|
|
27
|
+
throw new EmailMessageValidationError(`Nodemailer ${label} must not contain newline characters.`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
24
30
|
function createAddress(address) {
|
|
25
|
-
|
|
31
|
+
assertSafeAddressPart(address.address, 'address');
|
|
32
|
+
if (address.name) {
|
|
33
|
+
assertSafeAddressPart(address.name, 'display name');
|
|
34
|
+
return {
|
|
35
|
+
address: address.address,
|
|
36
|
+
name: address.name
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return address.address;
|
|
26
40
|
}
|
|
27
41
|
function createAddressList(addresses) {
|
|
28
42
|
if (addresses.length === 0) {
|
package/dist/service.d.ts
CHANGED
|
@@ -11,11 +11,13 @@ import type { Email, EmailMessage, EmailNotificationDispatchRequest, EmailSendBa
|
|
|
11
11
|
export declare class EmailService implements Email, OnModuleInit, OnApplicationShutdown {
|
|
12
12
|
private readonly options;
|
|
13
13
|
private lifecycleState;
|
|
14
|
+
private bootstrapPromise;
|
|
14
15
|
private resolvedTransport;
|
|
15
16
|
private transportPromise;
|
|
16
17
|
constructor(options: NormalizedEmailModuleOptions);
|
|
17
18
|
onApplicationShutdown(): Promise<void>;
|
|
18
19
|
onModuleInit(): Promise<void>;
|
|
20
|
+
private startTransport;
|
|
19
21
|
/**
|
|
20
22
|
* Creates a platform status snapshot for the active email transport wiring.
|
|
21
23
|
*
|
|
@@ -75,6 +77,10 @@ export declare class EmailService implements Email, OnModuleInit, OnApplicationS
|
|
|
75
77
|
*/
|
|
76
78
|
sendNotification(notification: EmailNotificationDispatchRequest, options?: EmailSendOptions): Promise<EmailSendResult>;
|
|
77
79
|
private ensureTransport;
|
|
80
|
+
private ensureReadyForDelivery;
|
|
81
|
+
private getLifecycleState;
|
|
82
|
+
private assertCanCreateOrUseTransport;
|
|
83
|
+
private assertCanDeliver;
|
|
78
84
|
private normalizeMessage;
|
|
79
85
|
private renderNotification;
|
|
80
86
|
}
|
package/dist/service.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAM3E,OAAO,KAAK,EACV,KAAK,EAGL,YAAY,EACZ,gCAAgC,
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAM3E,OAAO,KAAK,EACV,KAAK,EAGL,YAAY,EACZ,gCAAgC,EAChC,oBAAoB,EAEpB,oBAAoB,EACpB,gBAAgB,EAChB,eAAe,EAKf,4BAA4B,EAC7B,MAAM,YAAY,CAAC;AAqEpB;;;;;;;GAOG;AACH,qBACa,YAAa,YAAW,KAAK,EAAE,YAAY,EAAE,qBAAqB;IAMjE,OAAO,CAAC,QAAQ,CAAC,OAAO;IALpC,OAAO,CAAC,cAAc,CAAyC;IAC/D,OAAO,CAAC,gBAAgB,CAA4B;IACpD,OAAO,CAAC,iBAAiB,CAA6B;IACtD,OAAO,CAAC,gBAAgB,CAAsC;gBAEjC,OAAO,EAAE,4BAA4B;IAE5D,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IAiBtC,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;YAgBrB,cAAc;IAiC5B;;;;OAIG;IACH,4BAA4B;IAY5B;;;;;;;;;;;;;;;;;OAiBG;IACG,IAAI,CAAC,OAAO,EAAE,YAAY,EAAE,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,eAAe,CAAC;IA6B3F;;;;;;;;;;;;OAYG;IACG,QAAQ,CAAC,QAAQ,EAAE,SAAS,YAAY,EAAE,EAAE,OAAO,GAAE,oBAAyB,GAAG,OAAO,CAAC,oBAAoB,CAAC;IA6BpH;;;;;;;;;;;;;;;;;OAiBG;IACG,gBAAgB,CACpB,YAAY,EAAE,gCAAgC,EAC9C,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,eAAe,CAAC;YA+Bb,eAAe;YAiBf,sBAAsB;IAwBpC,OAAO,CAAC,iBAAiB;IAIzB,OAAO,CAAC,6BAA6B;IAMrC,OAAO,CAAC,gBAAgB;IAIxB,OAAO,CAAC,gBAAgB;YAuBV,kBAAkB;CAkBjC"}
|
package/dist/service.js
CHANGED
|
@@ -6,7 +6,7 @@ function _setFunctionName(e, t, n) { "symbol" == typeof t && (t = (t = t.descrip
|
|
|
6
6
|
function _checkInRHS(e) { if (Object(e) !== e) throw TypeError("right-hand side of 'in' should be an object, got " + (null !== e ? typeof e : "null")); return e; }
|
|
7
7
|
import { Inject } from '@fluojs/core';
|
|
8
8
|
import { DEFAULT_EMAIL_QUEUE_WORKER_OPTIONS } from './constants.js';
|
|
9
|
-
import { EmailMessageValidationError } from './errors.js';
|
|
9
|
+
import { EmailLifecycleError, EmailMessageValidationError } from './errors.js';
|
|
10
10
|
import { createEmailPlatformStatusSnapshot } from './status.js';
|
|
11
11
|
import { EMAIL_OPTIONS } from './tokens.js';
|
|
12
12
|
function normalizeAddress(address) {
|
|
@@ -35,10 +35,29 @@ function createAbortError() {
|
|
|
35
35
|
error.name = 'AbortError';
|
|
36
36
|
return error;
|
|
37
37
|
}
|
|
38
|
+
function assertNotAborted(signal) {
|
|
39
|
+
if (signal?.aborted) {
|
|
40
|
+
throw createAbortError();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function createLifecycleError(message, cause) {
|
|
44
|
+
return new EmailLifecycleError(message, {
|
|
45
|
+
cause
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function createDeliveryLifecycleError(state) {
|
|
49
|
+
return new EmailLifecycleError(`Email delivery cannot start while the service lifecycle is ${state}.`);
|
|
50
|
+
}
|
|
51
|
+
function isShutdownLifecycleState(state) {
|
|
52
|
+
return state === 'stopping' || state === 'stopped';
|
|
53
|
+
}
|
|
38
54
|
function assertMessageContent(message) {
|
|
39
55
|
if (message.to.length === 0) {
|
|
40
56
|
throw new EmailMessageValidationError('Email messages require at least one recipient in `to`.');
|
|
41
57
|
}
|
|
58
|
+
if (message.to.some(entry => entry.address.length === 0)) {
|
|
59
|
+
throw new EmailMessageValidationError('Email messages require non-empty recipients in `to`.');
|
|
60
|
+
}
|
|
42
61
|
if (!message.from.address) {
|
|
43
62
|
throw new EmailMessageValidationError('Email messages require a resolved `from` address.');
|
|
44
63
|
}
|
|
@@ -61,6 +80,7 @@ class EmailService {
|
|
|
61
80
|
[_EmailService, _initClass] = _applyDecs(this, [Inject(EMAIL_OPTIONS)], []).c;
|
|
62
81
|
}
|
|
63
82
|
lifecycleState = 'created';
|
|
83
|
+
bootstrapPromise;
|
|
64
84
|
resolvedTransport;
|
|
65
85
|
transportPromise;
|
|
66
86
|
constructor(options) {
|
|
@@ -69,26 +89,52 @@ class EmailService {
|
|
|
69
89
|
async onApplicationShutdown() {
|
|
70
90
|
this.lifecycleState = 'stopping';
|
|
71
91
|
try {
|
|
72
|
-
|
|
73
|
-
|
|
92
|
+
const transport = this.resolvedTransport ?? (this.transportPromise ? await this.transportPromise : undefined);
|
|
93
|
+
if (transport && this.options.transport.ownsResources && transport.close) {
|
|
94
|
+
await transport.close();
|
|
74
95
|
}
|
|
75
96
|
this.lifecycleState = 'stopped';
|
|
76
|
-
} catch {
|
|
97
|
+
} catch (error) {
|
|
77
98
|
this.lifecycleState = 'failed';
|
|
78
|
-
throw
|
|
99
|
+
throw createLifecycleError('Email transport failed to close cleanly.', error);
|
|
79
100
|
}
|
|
80
101
|
}
|
|
81
102
|
async onModuleInit() {
|
|
103
|
+
if (this.bootstrapPromise) {
|
|
104
|
+
return this.bootstrapPromise;
|
|
105
|
+
}
|
|
106
|
+
this.bootstrapPromise = this.startTransport();
|
|
107
|
+
try {
|
|
108
|
+
await this.bootstrapPromise;
|
|
109
|
+
} finally {
|
|
110
|
+
if (this.lifecycleState === 'failed') {
|
|
111
|
+
this.bootstrapPromise = undefined;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async startTransport() {
|
|
116
|
+
if (this.lifecycleState === 'stopping' || this.lifecycleState === 'stopped') {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
82
119
|
this.lifecycleState = 'starting';
|
|
83
120
|
try {
|
|
84
121
|
const transport = await this.ensureTransport();
|
|
122
|
+
if (this.lifecycleState !== 'starting') {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
85
125
|
if (this.options.verifyOnModuleInit && transport.verify) {
|
|
86
126
|
await transport.verify();
|
|
87
127
|
}
|
|
128
|
+
if (this.lifecycleState !== 'starting') {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
88
131
|
this.lifecycleState = 'ready';
|
|
89
|
-
} catch {
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (isShutdownLifecycleState(this.lifecycleState)) {
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
90
136
|
this.lifecycleState = 'failed';
|
|
91
|
-
throw
|
|
137
|
+
throw createLifecycleError('Email transport failed to initialize.', error);
|
|
92
138
|
}
|
|
93
139
|
}
|
|
94
140
|
|
|
@@ -128,12 +174,21 @@ class EmailService {
|
|
|
128
174
|
* ```
|
|
129
175
|
*/
|
|
130
176
|
async send(message, options = {}) {
|
|
131
|
-
|
|
132
|
-
|
|
177
|
+
assertNotAborted(options.signal);
|
|
178
|
+
if (this.options.verifyOnModuleInit) {
|
|
179
|
+
await this.ensureReadyForDelivery();
|
|
180
|
+
} else {
|
|
181
|
+
this.assertCanDeliver();
|
|
133
182
|
}
|
|
134
183
|
const transport = await this.ensureTransport();
|
|
135
184
|
const normalized = this.normalizeMessage(message);
|
|
136
185
|
assertMessageContent(normalized);
|
|
186
|
+
assertNotAborted(options.signal);
|
|
187
|
+
if (this.options.verifyOnModuleInit) {
|
|
188
|
+
await this.ensureReadyForDelivery();
|
|
189
|
+
} else {
|
|
190
|
+
this.assertCanDeliver();
|
|
191
|
+
}
|
|
137
192
|
const result = await transport.send(normalized, options);
|
|
138
193
|
return {
|
|
139
194
|
accepted: result.accepted ?? [],
|
|
@@ -202,8 +257,11 @@ class EmailService {
|
|
|
202
257
|
* ```
|
|
203
258
|
*/
|
|
204
259
|
async sendNotification(notification, options = {}) {
|
|
260
|
+
assertNotAborted(options.signal);
|
|
261
|
+
this.assertCanDeliver();
|
|
205
262
|
const payload = notification.payload;
|
|
206
|
-
const rendered = await this.renderNotification(notification);
|
|
263
|
+
const rendered = await this.renderNotification(notification, options.signal);
|
|
264
|
+
assertNotAborted(options.signal);
|
|
207
265
|
return this.send({
|
|
208
266
|
attachments: payload.attachments,
|
|
209
267
|
bcc: payload.bcc,
|
|
@@ -225,6 +283,7 @@ class EmailService {
|
|
|
225
283
|
}, options);
|
|
226
284
|
}
|
|
227
285
|
async ensureTransport() {
|
|
286
|
+
this.assertCanCreateOrUseTransport();
|
|
228
287
|
if (this.resolvedTransport) {
|
|
229
288
|
return this.resolvedTransport;
|
|
230
289
|
}
|
|
@@ -236,6 +295,34 @@ class EmailService {
|
|
|
236
295
|
}
|
|
237
296
|
return this.transportPromise;
|
|
238
297
|
}
|
|
298
|
+
async ensureReadyForDelivery() {
|
|
299
|
+
this.assertCanDeliver();
|
|
300
|
+
if (!this.options.verifyOnModuleInit) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (this.lifecycleState === 'ready') {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (this.lifecycleState === 'created' || this.lifecycleState === 'starting') {
|
|
307
|
+
await this.onModuleInit();
|
|
308
|
+
}
|
|
309
|
+
this.assertCanDeliver();
|
|
310
|
+
const state = this.getLifecycleState();
|
|
311
|
+
if (state !== 'ready') {
|
|
312
|
+
throw createDeliveryLifecycleError(state);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
getLifecycleState() {
|
|
316
|
+
return this.lifecycleState;
|
|
317
|
+
}
|
|
318
|
+
assertCanCreateOrUseTransport() {
|
|
319
|
+
if (this.lifecycleState === 'stopping' || this.lifecycleState === 'stopped' || this.lifecycleState === 'failed') {
|
|
320
|
+
throw createDeliveryLifecycleError(this.lifecycleState);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
assertCanDeliver() {
|
|
324
|
+
this.assertCanCreateOrUseTransport();
|
|
325
|
+
}
|
|
239
326
|
normalizeMessage(message) {
|
|
240
327
|
const from = message.from ? normalizeAddress(message.from) : this.options.defaultFrom;
|
|
241
328
|
const replyTo = normalizeAddressList(message.replyTo);
|
|
@@ -256,10 +343,11 @@ class EmailService {
|
|
|
256
343
|
to: normalizeAddressList(message.to)
|
|
257
344
|
};
|
|
258
345
|
}
|
|
259
|
-
async renderNotification(notification) {
|
|
346
|
+
async renderNotification(notification, signal) {
|
|
260
347
|
if (!notification.template || !this.options.renderer) {
|
|
261
348
|
return undefined;
|
|
262
349
|
}
|
|
350
|
+
assertNotAborted(signal);
|
|
263
351
|
return this.options.renderer.render({
|
|
264
352
|
locale: notification.locale,
|
|
265
353
|
metadata: notification.metadata,
|
package/package.json
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"queue",
|
|
11
11
|
"mailer"
|
|
12
12
|
],
|
|
13
|
-
"version": "1.0.0-beta.
|
|
13
|
+
"version": "1.0.0-beta.5",
|
|
14
14
|
"private": false,
|
|
15
15
|
"license": "MIT",
|
|
16
16
|
"repository": {
|
|
@@ -52,14 +52,14 @@
|
|
|
52
52
|
"dist"
|
|
53
53
|
],
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@fluojs/core": "^1.0.0-beta.
|
|
56
|
-
"@fluojs/
|
|
57
|
-
"@fluojs/
|
|
58
|
-
"@fluojs/runtime": "^1.0.0-beta.
|
|
55
|
+
"@fluojs/core": "^1.0.0-beta.6",
|
|
56
|
+
"@fluojs/notifications": "^1.0.0-beta.5",
|
|
57
|
+
"@fluojs/di": "^1.0.0-beta.8",
|
|
58
|
+
"@fluojs/runtime": "^1.0.0-beta.12"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"nodemailer": "^6.10.1",
|
|
62
|
-
"@fluojs/queue": "^1.0.0-beta.
|
|
62
|
+
"@fluojs/queue": "^1.0.0-beta.5"
|
|
63
63
|
},
|
|
64
64
|
"peerDependenciesMeta": {
|
|
65
65
|
"@fluojs/queue": {
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"devDependencies": {
|
|
73
73
|
"@types/nodemailer": "^8.0.0",
|
|
74
74
|
"vitest": "^3.2.4",
|
|
75
|
-
"@fluojs/queue": "^1.0.0-beta.
|
|
75
|
+
"@fluojs/queue": "^1.0.0-beta.5"
|
|
76
76
|
},
|
|
77
77
|
"scripts": {
|
|
78
78
|
"prebuild": "node ../../tooling/scripts/clean-dist.mjs",
|