@fluojs/email 1.0.0-beta.4 → 1.0.0

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 CHANGED
@@ -140,6 +140,7 @@ Behavioral contract 메모:
140
140
  - `createNodemailerEmailTransportFactory(...)`는 Node 전용이며 `@fluojs/email/node`에서만 export됩니다.
141
141
  - 이 factory는 자신이 생성한 Nodemailer transporter 리소스를 소유하므로 `EmailService`가 bootstrap 시 검증하고 shutdown 시 닫을 수 있습니다.
142
142
  - `createNodemailerEmailTransport(...)`는 이미 존재하는 Nodemailer transporter를 감싸지만 리소스 소유권은 호출자에게 남깁니다.
143
+ - Nodemailer display-name 주소는 구조화된 address object로 전달되며, newline 문자가 포함되면 provider handoff 전에 거부됩니다.
143
144
  - SMTP 자격 증명은 여전히 명시적인 옵션 또는 DI를 통해 들어와야 합니다. 루트 패키지와 Node 서브패스 모두 `process.env`를 직접 읽지 않습니다.
144
145
 
145
146
  ### `EmailService`를 이용한 standalone 전달
@@ -168,7 +169,9 @@ Behavioral contract 메모:
168
169
  - `EmailService.send(...)`는 `accepted`, `pending`, `rejected` 수신자를 분리해 보존하므로 provider의 부분 실패가 호출자에게 그대로 보입니다.
169
170
  - `EmailService.sendMany(...)`는 기본적으로 fail-fast입니다. 실패를 batch result에 수집하려면 `continueOnError: true`를 전달합니다.
170
171
  - `EmailService.createPlatformStatusSnapshot()`은 diagnostics를 위해 lifecycle, readiness, health, transport ownership details를 노출합니다.
171
- - 서비스는 모듈 bootstrap 시 transport를 초기화하고, factory가 소유한 리소스만 애플리케이션 shutdown 닫습니다.
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이 기다린 뒤 닫습니다.
172
175
  - transport `verify()`와 `close()`에서 발생한 provider error는 diagnostics를 위해 lifecycle failure의 `cause`로 보존됩니다.
173
176
  - 모듈 옵션은 provider wiring 전에 trim 및 normalize됩니다. 여기에는 sender 기본값, notification channel 이름, transport factory 소유권이 포함됩니다.
174
177
  - 이 패키지는 절대로 `process.env`를 직접 읽지 않습니다. 모든 설정은 명시적인 옵션 또는 DI를 통해 들어와야 합니다.
@@ -213,6 +216,7 @@ Behavioral contract 메모:
213
216
 
214
217
  - `EmailChannel`은 `pending` 또는 `rejected` 수신자가 하나라도 있으면 전달을 성공으로 보고하지 않고 notification dispatch를 실패로 처리합니다.
215
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보다 우선합니다.
216
220
 
217
221
  ### 큐 기반 대량 전달
218
222
 
@@ -310,6 +314,7 @@ email 패키지는 의도적으로 다음을 **포함하지 않습니다**:
310
314
 
311
315
  - `createEmailPlatformStatusSnapshot(...)`
312
316
  - `EmailConfigurationError`
317
+ - `EmailLifecycleError`: lifecycle로 차단된 전달, transport 초기화 또는 검증, 소유 리소스 shutdown 실패에서 발생합니다. 애플리케이션 teardown과 전송이 경합할 수 있다면 이 에러를 catch하세요.
313
318
  - `EmailMessageValidationError`
314
319
 
315
320
  ### Node 전용 서브패스
package/README.md CHANGED
@@ -140,6 +140,7 @@ Behavioral contract notes:
140
140
  - `createNodemailerEmailTransportFactory(...)` is Node-only and is exported exclusively from `@fluojs/email/node`.
141
141
  - The factory owns the Nodemailer transporter it creates, so `EmailService` can verify it on bootstrap and close it during shutdown.
142
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.
143
144
  - SMTP credentials still enter through explicit options or DI. Neither the root package nor the Node subpath reads `process.env` directly.
144
145
 
145
146
  ### Standalone delivery with `EmailService`
@@ -168,7 +169,9 @@ Behavioral contract notes:
168
169
  - `EmailService.send(...)` preserves `accepted`, `pending`, and `rejected` recipients separately so partial provider failures stay caller-visible.
169
170
  - `EmailService.sendMany(...)` is fail-fast by default; pass `continueOnError: true` to collect failures in a batch result.
170
171
  - `EmailService.createPlatformStatusSnapshot()` exposes lifecycle, readiness, health, and transport ownership details for diagnostics.
171
- - The service initializes the configured transport during module bootstrap and closes factory-owned resources during application shutdown.
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.
172
175
  - Transport `verify()` and `close()` provider errors are preserved as the `cause` of lifecycle failures for diagnostics.
173
176
  - Module options are trimmed and normalized before provider wiring, including sender defaults, notification channel names, and transport factory ownership.
174
177
  - The package never reads `process.env` directly. All configuration must enter through explicit options or DI.
@@ -213,6 +216,7 @@ Behavioral contract notes:
213
216
 
214
217
  - `EmailChannel` treats any `pending` or `rejected` recipients as a failed notification dispatch instead of reporting the delivery as successful.
215
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.
216
220
 
217
221
  ### Queue-backed bulk delivery
218
222
 
@@ -310,6 +314,7 @@ These limitations are part of the package contract so transport selection, templ
310
314
 
311
315
  - `createEmailPlatformStatusSnapshot(...)`
312
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.
313
318
  - `EmailMessageValidationError`
314
319
 
315
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
@@ -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';
@@ -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';
@@ -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;AAyHpB,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"}
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;AAErB,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;AAoDD;;;;;;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"}
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"}
@@ -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
- return address.name ? `${address.name} <${address.address}>` : address.address;
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
  }
@@ -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,EAC9B,oBAAoB,EAEpB,oBAAoB,EACpB,gBAAgB,EAChB,eAAe,EAKf,4BAA4B,EAC7B,MAAM,YAAY,CAAC;AAyDtB;;;;;;;GAOG;AACH,qBACa,YAAa,YAAW,KAAK,EAAE,YAAY,EAAE,qBAAqB;IAKjE,OAAO,CAAC,QAAQ,CAAC,OAAO;IAJpC,OAAO,CAAC,cAAc,CAAmF;IACzG,OAAO,CAAC,iBAAiB,CAA6B;IACtD,OAAO,CAAC,gBAAgB,CAAsC;gBAEjC,OAAO,EAAE,4BAA4B;IAE5D,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IAetC,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IAiBnC;;;;OAIG;IACH,4BAA4B;IAY5B;;;;;;;;;;;;;;;;;OAiBG;IACG,IAAI,CAAC,OAAO,EAAE,YAAY,EAAE,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,eAAe,CAAC;IAmB3F;;;;;;;;;;;;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;YA4Bb,eAAe;IAe7B,OAAO,CAAC,gBAAgB;YAuBV,kBAAkB;CAkBjC"}
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) {
@@ -41,10 +41,16 @@ function assertNotAborted(signal) {
41
41
  }
42
42
  }
43
43
  function createLifecycleError(message, cause) {
44
- return new Error(message, {
44
+ return new EmailLifecycleError(message, {
45
45
  cause
46
46
  });
47
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
+ }
48
54
  function assertMessageContent(message) {
49
55
  if (message.to.length === 0) {
50
56
  throw new EmailMessageValidationError('Email messages require at least one recipient in `to`.');
@@ -74,6 +80,7 @@ class EmailService {
74
80
  [_EmailService, _initClass] = _applyDecs(this, [Inject(EMAIL_OPTIONS)], []).c;
75
81
  }
76
82
  lifecycleState = 'created';
83
+ bootstrapPromise;
77
84
  resolvedTransport;
78
85
  transportPromise;
79
86
  constructor(options) {
@@ -82,8 +89,9 @@ class EmailService {
82
89
  async onApplicationShutdown() {
83
90
  this.lifecycleState = 'stopping';
84
91
  try {
85
- if (this.resolvedTransport && this.options.transport.ownsResources && this.resolvedTransport.close) {
86
- await this.resolvedTransport.close();
92
+ const transport = this.resolvedTransport ?? (this.transportPromise ? await this.transportPromise : undefined);
93
+ if (transport && this.options.transport.ownsResources && transport.close) {
94
+ await transport.close();
87
95
  }
88
96
  this.lifecycleState = 'stopped';
89
97
  } catch (error) {
@@ -92,14 +100,39 @@ class EmailService {
92
100
  }
93
101
  }
94
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
+ }
95
119
  this.lifecycleState = 'starting';
96
120
  try {
97
121
  const transport = await this.ensureTransport();
122
+ if (this.lifecycleState !== 'starting') {
123
+ return;
124
+ }
98
125
  if (this.options.verifyOnModuleInit && transport.verify) {
99
126
  await transport.verify();
100
127
  }
128
+ if (this.lifecycleState !== 'starting') {
129
+ return;
130
+ }
101
131
  this.lifecycleState = 'ready';
102
132
  } catch (error) {
133
+ if (isShutdownLifecycleState(this.lifecycleState)) {
134
+ throw error;
135
+ }
103
136
  this.lifecycleState = 'failed';
104
137
  throw createLifecycleError('Email transport failed to initialize.', error);
105
138
  }
@@ -142,10 +175,20 @@ class EmailService {
142
175
  */
143
176
  async send(message, options = {}) {
144
177
  assertNotAborted(options.signal);
178
+ if (this.options.verifyOnModuleInit) {
179
+ await this.ensureReadyForDelivery();
180
+ } else {
181
+ this.assertCanDeliver();
182
+ }
145
183
  const transport = await this.ensureTransport();
146
184
  const normalized = this.normalizeMessage(message);
147
185
  assertMessageContent(normalized);
148
186
  assertNotAborted(options.signal);
187
+ if (this.options.verifyOnModuleInit) {
188
+ await this.ensureReadyForDelivery();
189
+ } else {
190
+ this.assertCanDeliver();
191
+ }
149
192
  const result = await transport.send(normalized, options);
150
193
  return {
151
194
  accepted: result.accepted ?? [],
@@ -214,6 +257,8 @@ class EmailService {
214
257
  * ```
215
258
  */
216
259
  async sendNotification(notification, options = {}) {
260
+ assertNotAborted(options.signal);
261
+ this.assertCanDeliver();
217
262
  const payload = notification.payload;
218
263
  const rendered = await this.renderNotification(notification, options.signal);
219
264
  assertNotAborted(options.signal);
@@ -238,6 +283,7 @@ class EmailService {
238
283
  }, options);
239
284
  }
240
285
  async ensureTransport() {
286
+ this.assertCanCreateOrUseTransport();
241
287
  if (this.resolvedTransport) {
242
288
  return this.resolvedTransport;
243
289
  }
@@ -249,6 +295,34 @@ class EmailService {
249
295
  }
250
296
  return this.transportPromise;
251
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
+ }
252
326
  normalizeMessage(message) {
253
327
  const from = message.from ? normalizeAddress(message.from) : this.options.defaultFrom;
254
328
  const replyTo = normalizeAddressList(message.replyTo);
package/package.json CHANGED
@@ -10,7 +10,7 @@
10
10
  "queue",
11
11
  "mailer"
12
12
  ],
13
- "version": "1.0.0-beta.4",
13
+ "version": "1.0.0",
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.5",
56
- "@fluojs/di": "^1.0.0-beta.7",
57
- "@fluojs/notifications": "^1.0.0-beta.4",
58
- "@fluojs/runtime": "^1.0.0-beta.12"
55
+ "@fluojs/core": "^1.0.0",
56
+ "@fluojs/di": "^1.0.0",
57
+ "@fluojs/notifications": "^1.0.0",
58
+ "@fluojs/runtime": "^1.0.0"
59
59
  },
60
60
  "peerDependencies": {
61
61
  "nodemailer": "^6.10.1",
62
- "@fluojs/queue": "^1.0.0-beta.5"
62
+ "@fluojs/queue": "^1.0.0"
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.5"
75
+ "@fluojs/queue": "^1.0.0"
76
76
  },
77
77
  "scripts": {
78
78
  "prebuild": "node ../../tooling/scripts/clean-dist.mjs",