@fluojs/email 1.0.0-beta.3 → 1.0.0-beta.4

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
@@ -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(@Inject(EmailService) private readonly email: EmailService) {}
91
+ constructor(private readonly email: EmailService) {}
91
92
 
92
93
  async sendWelcome(address: string) {
93
94
  await this.email.send({
@@ -162,10 +163,13 @@ EmailModule.forRootAsync({
162
163
  Behavioral contract 메모:
163
164
 
164
165
  - `EmailService.send(...)`는 전달 전에 `defaultFrom`과 `defaultReplyTo`를 해석합니다.
166
+ - `EmailService.send(...)`는 빈 `to` 수신자를 transport handoff 전에 거부하므로 transport가 빈 전달 대상을 받지 않습니다.
167
+ - `EmailService.send(...)`와 `EmailService.sendNotification(...)`은 이미 abort된 `AbortSignal`을 템플릿 렌더링 또는 transport handoff 전에 반영합니다.
165
168
  - `EmailService.send(...)`는 `accepted`, `pending`, `rejected` 수신자를 분리해 보존하므로 provider의 부분 실패가 호출자에게 그대로 보입니다.
166
169
  - `EmailService.sendMany(...)`는 기본적으로 fail-fast입니다. 실패를 batch result에 수집하려면 `continueOnError: true`를 전달합니다.
167
170
  - `EmailService.createPlatformStatusSnapshot()`은 diagnostics를 위해 lifecycle, readiness, health, transport ownership details를 노출합니다.
168
171
  - 서비스는 모듈 bootstrap 시 transport를 초기화하고, factory가 소유한 리소스만 애플리케이션 shutdown 시 닫습니다.
172
+ - transport `verify()`와 `close()`에서 발생한 provider error는 diagnostics를 위해 lifecycle failure의 `cause`로 보존됩니다.
169
173
  - 모듈 옵션은 provider wiring 전에 trim 및 normalize됩니다. 여기에는 sender 기본값, notification channel 이름, transport factory 소유권이 포함됩니다.
170
174
  - 이 패키지는 절대로 `process.env`를 직접 읽지 않습니다. 모든 설정은 명시적인 옵션 또는 DI를 통해 들어와야 합니다.
171
175
 
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(@Inject(EmailService) private readonly email: EmailService) {}
91
+ constructor(private readonly email: EmailService) {}
91
92
 
92
93
  async sendWelcome(address: string) {
93
94
  await this.email.send({
@@ -162,10 +163,13 @@ EmailModule.forRootAsync({
162
163
  Behavioral contract notes:
163
164
 
164
165
  - `EmailService.send(...)` resolves `defaultFrom` and `defaultReplyTo` before delivery.
166
+ - `EmailService.send(...)` rejects blank `to` recipients before handoff so transports never receive an empty delivery target.
167
+ - `EmailService.send(...)` and `EmailService.sendNotification(...)` honor an already-aborted `AbortSignal` before template rendering or transport handoff.
165
168
  - `EmailService.send(...)` preserves `accepted`, `pending`, and `rejected` recipients separately so partial provider failures stay caller-visible.
166
169
  - `EmailService.sendMany(...)` is fail-fast by default; pass `continueOnError: true` to collect failures in a batch result.
167
170
  - `EmailService.createPlatformStatusSnapshot()` exposes lifecycle, readiness, health, and transport ownership details for diagnostics.
168
171
  - The service initializes the configured transport during module bootstrap and closes factory-owned resources during application shutdown.
172
+ - Transport `verify()` and `close()` provider errors are preserved as the `cause` of lifecycle failures for diagnostics.
169
173
  - Module options are trimmed and normalized before provider wiring, including sender defaults, notification channel names, and transport factory ownership.
170
174
  - The package never reads `process.env` directly. All configuration must enter through explicit options or DI.
171
175
 
@@ -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;AA2CtB;;;;;;;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;IAoB3F;;;;;;;;;;;;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;YA0Bb,eAAe;IAe7B,OAAO,CAAC,gBAAgB;YAuBV,kBAAkB;CAejC"}
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"}
package/dist/service.js CHANGED
@@ -35,10 +35,23 @@ 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 Error(message, {
45
+ cause
46
+ });
47
+ }
38
48
  function assertMessageContent(message) {
39
49
  if (message.to.length === 0) {
40
50
  throw new EmailMessageValidationError('Email messages require at least one recipient in `to`.');
41
51
  }
52
+ if (message.to.some(entry => entry.address.length === 0)) {
53
+ throw new EmailMessageValidationError('Email messages require non-empty recipients in `to`.');
54
+ }
42
55
  if (!message.from.address) {
43
56
  throw new EmailMessageValidationError('Email messages require a resolved `from` address.');
44
57
  }
@@ -73,9 +86,9 @@ class EmailService {
73
86
  await this.resolvedTransport.close();
74
87
  }
75
88
  this.lifecycleState = 'stopped';
76
- } catch {
89
+ } catch (error) {
77
90
  this.lifecycleState = 'failed';
78
- throw new Error('Email transport failed to close cleanly.');
91
+ throw createLifecycleError('Email transport failed to close cleanly.', error);
79
92
  }
80
93
  }
81
94
  async onModuleInit() {
@@ -86,9 +99,9 @@ class EmailService {
86
99
  await transport.verify();
87
100
  }
88
101
  this.lifecycleState = 'ready';
89
- } catch {
102
+ } catch (error) {
90
103
  this.lifecycleState = 'failed';
91
- throw new Error('Email transport failed to initialize.');
104
+ throw createLifecycleError('Email transport failed to initialize.', error);
92
105
  }
93
106
  }
94
107
 
@@ -128,12 +141,11 @@ class EmailService {
128
141
  * ```
129
142
  */
130
143
  async send(message, options = {}) {
131
- if (options.signal?.aborted) {
132
- throw createAbortError();
133
- }
144
+ assertNotAborted(options.signal);
134
145
  const transport = await this.ensureTransport();
135
146
  const normalized = this.normalizeMessage(message);
136
147
  assertMessageContent(normalized);
148
+ assertNotAborted(options.signal);
137
149
  const result = await transport.send(normalized, options);
138
150
  return {
139
151
  accepted: result.accepted ?? [],
@@ -203,7 +215,8 @@ class EmailService {
203
215
  */
204
216
  async sendNotification(notification, options = {}) {
205
217
  const payload = notification.payload;
206
- const rendered = await this.renderNotification(notification);
218
+ const rendered = await this.renderNotification(notification, options.signal);
219
+ assertNotAborted(options.signal);
207
220
  return this.send({
208
221
  attachments: payload.attachments,
209
222
  bcc: payload.bcc,
@@ -256,10 +269,11 @@ class EmailService {
256
269
  to: normalizeAddressList(message.to)
257
270
  };
258
271
  }
259
- async renderNotification(notification) {
272
+ async renderNotification(notification, signal) {
260
273
  if (!notification.template || !this.options.renderer) {
261
274
  return undefined;
262
275
  }
276
+ assertNotAborted(signal);
263
277
  return this.options.renderer.render({
264
278
  locale: notification.locale,
265
279
  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.3",
13
+ "version": "1.0.0-beta.4",
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.4",
56
- "@fluojs/di": "^1.0.0-beta.6",
57
- "@fluojs/notifications": "^1.0.0-beta.3",
58
- "@fluojs/runtime": "^1.0.0-beta.11"
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"
59
59
  },
60
60
  "peerDependencies": {
61
61
  "nodemailer": "^6.10.1",
62
- "@fluojs/queue": "^1.0.0-beta.4"
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.4"
75
+ "@fluojs/queue": "^1.0.0-beta.5"
76
76
  },
77
77
  "scripts": {
78
78
  "prebuild": "node ../../tooling/scripts/clean-dist.mjs",