@t4h.framework/smtp 0.7.6 → 0.7.9

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.
@@ -1,283 +1,104 @@
1
1
  ---
2
2
  name: framework-smtp
3
3
  description: >-
4
- Guides correct use of @t4h.framework/smtp in T4H Framework workflows: SMTP
5
- send-email activity (SmtpSendEmailActivity), SmtpSendEmailClaim, reconciler-bound
6
- Smtp client, recipients/cc/bcc normalization, HTML bodies, Binary attachments,
7
- and SmtpSendEmailOutput. Use when sending email from workflows, wiring SMTP
8
- claim providers, testing email activities, implementing SmtpSendEmailClaim,
9
- or working in packages/smtp.
4
+ Guides the use of @t4h.framework/smtp to send email from a T4H Framework
5
+ workflow: the Smtp client and its send() method, the default from, to/cc/bcc
6
+ recipients, HTML bodies, Binary attachments, SmtpSendEmailOutput.messageId,
7
+ and how to test with TestWorkflowEnvironment and mockClaim of
8
+ SmtpSendEmailClaim. Use when a workflow must send an email, a notification
9
+ or a report with an attachment, or when someone asks for nodemailer, an SMTP
10
+ transport or an smtp:// connection string in a workflow.
10
11
  ---
11
12
 
12
13
  # @t4h.framework/smtp
13
14
 
14
- SMTP activity, claim, and a reconciler-aware client for sending email inside workflows. Package path: `framework/packages/smtp`. Peer dependency: `@t4h.framework/core` (for `SyncActivity`, `Claim`, `History`, `Binary`, `SerializableClass`).
15
+ ## Send an email
15
16
 
16
- Import surface (the package entry; its `exports` map has only `.`, so `tsc` rejects a deep import):
17
-
18
- ```typescript
19
- import {
20
- Smtp,
21
- SmtpSendEmailActivity,
22
- SmtpSendEmailClaim,
23
- SmtpSendEmailOutput,
24
- type SmtpOptions,
25
- type SmtpSendEmailActivityOptions,
26
- type SmtpSendEmailActivityInput,
27
- type SmtpSendEmailActivityAttachment,
28
- type ISmtpSendEmailOutput,
29
- } from '@t4h.framework/smtp'
30
- ```
31
-
32
- Node `>=22`. See [README.md](../../../README.md) for a short overview (note: README may say `sendEmail` on the claim; the actual method is **`send`**).
33
-
34
- ---
35
-
36
- ## Architecture
37
-
38
- `Workflow` → `Smtp.send` (`History.bind(SmtpSendEmailActivity)`) → `SmtpSendEmailActivity.toInput` / `run` → `SmtpSendEmailClaim.send` → `SmtpSendEmailOutput`.
39
-
40
- ```
41
- ┌─────────────┐ send() ┌────────────────────────┐ run() ┌─────────────────────┐
42
- │ Smtp │ ──────────────► │ SmtpSendEmailActivity │ ────────────► │ SmtpSendEmailClaim │
43
- │ (client) │ reconciler │ (SyncActivity) │ Claim.smtp │ (runtime impl) │
44
- └─────────────┘ └────────────────────────┘ └─────────────────────┘
45
- ```
46
-
47
- `@t4h.framework/core/no-io-in-workflow` rejects `nodemailer` and raw sockets in a workflow. Use `Smtp`.
48
-
49
- ---
50
-
51
- ## SmtpSendEmailClaim (required for delivery)
52
-
53
- Abstract dependency; the **runtime** must register a concrete implementation:
54
-
55
- ```typescript
56
- { provide: SmtpSendEmailClaim, value: new MySmtpSendEmailClaimImpl() }
57
- ```
58
-
59
- Contract (from source):
60
-
61
- ```typescript
62
- abstract class SmtpSendEmailClaim {
63
- abstract send(
64
- input: SmtpSendEmailActivityInput,
65
- ): ISmtpSendEmailOutput | Promise<ISmtpSendEmailOutput>
66
- }
67
-
68
- type ISmtpSendEmailOutput = { messageId: string }
69
- ```
70
-
71
- `SmtpSendEmailActivity.run` passes the full **`SmtpSendEmailActivityInput`** payload to `claim.send` (sync or async).
72
-
73
- ---
74
-
75
- ## Send options and payload
76
-
77
- The exported `SmtpSendEmailActivity` — driven by `smtp.send` or directly via
78
- `History.reconciler(SmtpSendEmailActivity, smtp, content)` — is replay-safe and
79
- delegates to `SmtpSendEmailClaim.send`, returning a `SmtpSendEmailOutput`. Its
80
- input shapes:
81
-
82
- ### SmtpSendEmailActivityOptions (what `smtp.send` accepts)
83
-
84
- | Field | Type | Required | Notes |
85
- |-------|------|----------|-------|
86
- | `to` | `string \| readonly string[]` | yes | Normalized to `readonly string[]` in input |
87
- | `subject` | `string` | yes | |
88
- | `html` | `string` | yes | HTML body |
89
- | `from` | `string?` | no | Overrides `smtp.options.from` |
90
- | `cc` | `string \| readonly string[]?` | no | String → single-element array |
91
- | `bcc` | `string \| readonly string[]?` | no | String → single-element array |
92
- | `attachments` | `readonly SmtpSendEmailActivityAttachment[]?` | no | Passed through unchanged |
93
-
94
- ### SmtpSendEmailActivityInput (claim / activity payload)
95
-
96
- | Field | Type | Notes |
97
- |-------|------|-------|
98
- | `connectionString` | `Secret` | From `smtp.connectionString`; the host reads the value when it sends |
99
- | `from` | `string?` | `content.from ?? smtp.options.from` |
100
- | `to` | `readonly string[]` | Always an array after `toInput` |
101
- | `cc` | `readonly string[]?` | Omitted if not set |
102
- | `bcc` | `readonly string[]?` | Omitted if not set |
103
- | `subject` | `string` | |
104
- | `html` | `string` | |
105
- | `attachments` | `readonly SmtpSendEmailActivityAttachment[]?` | |
106
-
107
- ### Attachments
108
-
109
- ```typescript
110
- import type { Binary } from '@t4h.framework/core'
111
-
112
- type SmtpSendEmailActivityAttachment = {
113
- filename: string
114
- content: Binary
115
- }
116
- ```
117
-
118
- Use framework `Binary` instances (e.g. from an HTTP `response.body`).
119
-
120
- ---
121
-
122
- ## Smtp client
123
-
124
- High-level API: holds `connectionString` and optional default `from`, extends `SerializableClass` for workflow persistence. `connectionString` is typed `Secret` (a string is a `tsc` error): pass the `EnvDesign` entry declared `type: Secret`, because the URL `smtp://user:pass@host:587` carries the password.
125
-
126
- ```typescript
127
- const smtp = new Smtp(environments.SMTP_URL, {
128
- from: 'noreply@example.com',
129
- })
130
-
131
- const result = await smtp.send({
132
- to: 'recipient@example.com',
133
- subject: 'Hello',
134
- html: '<p>Hello world</p>',
135
- })
136
-
137
- result.messageId // string — via SmtpSendEmailOutput
138
- ```
139
-
140
- - `send(options)` returns a promise of `SmtpSendEmailOutput` when history/reconciler completes (same pattern as `http` client).
141
- - Default `from`: set on `Smtp` constructor options; per-send `from` in `send()` wins over `smtp.options.from`.
142
- - `toSerializable()` / `fromSerializable()` restore `connectionString` and `options` (see `Smtp.spec.ts`).
143
-
144
- ### Multiple recipients, cc, bcc
145
-
146
- ```typescript
147
- await smtp.send({
148
- to: ['alice@example.com', 'bob@example.com'],
149
- cc: 'manager@example.com',
150
- bcc: ['audit@example.com'],
151
- subject: 'Team Update',
152
- html: '<p>Weekly report.</p>',
153
- })
154
- ```
155
-
156
- ### Using the exported activity directly via `History.reconciler`
157
-
158
- Prefer `smtp.send` in workflows. When you need the exported activity directly,
159
- drive the **shipped** `SmtpSendEmailActivity` through `History.reconciler` — a
160
- ready, replay-safe helper. `@t4h.framework/core/no-framework-activity-in-app` rejects `new` and `extends` on it:
161
-
162
- ```typescript
163
- import { History } from '@t4h.framework/core'
164
- import { Smtp, SmtpSendEmailActivity } from '@t4h.framework/smtp'
165
-
166
- const smtp = new Smtp(environments.SMTP_URL) // Secret
167
- const result = await History.reconciler(SmtpSendEmailActivity, smtp, {
168
- to: 'user@example.com',
169
- subject: 'Subject',
170
- html: '<p>Body</p>',
171
- })
172
- ```
173
-
174
- ---
175
-
176
- ## SmtpSendEmailOutput
177
-
178
- ```typescript
179
- class SmtpSendEmailOutput {
180
- constructor(public readonly raw: ISmtpSendEmailOutput)
181
- get messageId(): string
182
- }
183
- ```
184
-
185
- Activity and `Smtp.send` surface this type, not the raw `{ messageId }` object.
186
-
187
- ---
188
-
189
- ## Workflow example
17
+ 1. Declare the connection string as a `Secret` in the `EnvDesign` in
18
+ `src/constants/environments.ts`. The URL (`smtp://user:pass@host:587`)
19
+ carries the password.
20
+ 2. Create one `Smtp` client and call `send` in the workflow body.
190
21
 
191
22
  ```typescript
192
23
  // src/constants/environments.ts
193
24
  import { EnvDesign, Secret } from '@t4h.framework/core'
194
25
 
195
26
  export const environments = EnvDesign({
196
- SMTP_CONNECTION_STRING: { type: Secret }, // carries the password
197
- SMTP_FROM: { type: String },
27
+ SMTP_URL: { type: Secret },
198
28
  })
199
29
  ```
200
30
 
201
31
  ```typescript
202
- import { App, Workflow } from '@t4h.framework/core'
203
- import { http } from '@t4h.framework/http'
32
+ import { Workflow } from '@t4h.framework/core'
204
33
  import { Smtp } from '@t4h.framework/smtp'
205
34
  import Type from 'typebox'
206
35
 
207
- import { environments } from './constants/environments.js'
208
-
209
- const smtp = new Smtp(environments.SMTP_CONNECTION_STRING)
210
-
211
- export default new App({
212
- id: 'email',
213
- workflows: [
214
- new Workflow({ id: 'send-email', schema: Type.String() }, async input => {
215
- const response = await http.get('https://picsum.photos/300/200')
216
-
217
- await smtp.send({
218
- from: environments.SMTP_FROM,
219
- to: input,
220
- subject: 'Hello World!',
221
- html: '<p>...</p>',
222
- attachments: [{ filename: 'image.png', content: response.body.binary }],
223
- })
224
- }),
225
- ],
226
- })
36
+ import { environments } from '../constants/environments.js'
37
+
38
+ export const sendReport = new Workflow(
39
+ { id: 'send-report', schema: Type.Object({ email: Type.String() }) },
40
+ async input => {
41
+ const smtp = new Smtp(environments.SMTP_URL, {
42
+ from: 'noreply@example.com',
43
+ })
44
+
45
+ const result = await smtp.send({
46
+ to: input.email,
47
+ subject: 'Report',
48
+ html: '<p>See attached.</p>',
49
+ attachments: [{ filename: 'report.pdf', content: reportBinary }],
50
+ })
51
+
52
+ return { messageId: result.messageId }
53
+ },
54
+ )
227
55
  ```
228
56
 
229
- Connection strings are opaque to the package; dev implementation expects URL form with credentials in the authority (e.g. `smtp://user:pass@host:587`, `smtps://...`).
230
-
231
- ---
232
-
233
- ## Testing
234
-
235
- Run package tests: `yarn test` in `packages/smtp` (Vitest).
236
-
237
- Test the **workflow** with `TestWorkflowEnvironment` from
238
- `@t4h.framework/core/testing`, mocking `SmtpSendEmailClaim` — the single claim
239
- the send activity resolves. See the **framework-workflow-testing** skill for the
240
- harness API.
57
+ ## Traps
58
+
59
+ - The method is `send`, on the client and on `SmtpSendEmailClaim`. There is no
60
+ `sendEmail`.
61
+ - `new Smtp(...)` takes a `Secret`. A string literal or a `String` env entry is
62
+ a type error.
63
+ - Create the client in the workflow body. A test that imports the module has
64
+ no env, so a key read at module top level throws `No env storage`.
65
+ - The body field is `html`. There is no `text` field.
66
+ - An attachment `content` is a `Binary` (from `sftp.get`, an HTTP body, `write`
67
+ of `@t4h.framework/fs`). Never build a `Binary` by hand; see the
68
+ `framework-fs` skill.
69
+ - A `from` in `send` wins over the client default. Without both, `from` is
70
+ `undefined` and the claim decides.
71
+ - `@t4h.framework/core/no-io-in-workflow` rejects `nodemailer` in a workflow.
72
+ Do not call an SMTP library; use `Smtp`.
73
+ - Do not `new` or `extend` `SmtpSendEmailActivity`
74
+ (`@t4h.framework/core/no-framework-activity-in-app`). Use `smtp.send`.
75
+ - The runtime must provide `SmtpSendEmailClaim`.
76
+
77
+ ## Test
78
+
79
+ Mock `SmtpSendEmailClaim` with its `send` method. The mock receives the
80
+ normalized input: `to`, `cc` and `bcc` are always arrays.
241
81
 
242
82
  ```typescript
243
83
  import { TestWorkflowEnvironment, mockClaim } from '@t4h.framework/core/testing'
244
84
  import { SmtpSendEmailClaim } from '@t4h.framework/smtp'
85
+ import { expect, vi } from 'vitest'
245
86
 
246
- const mockSend = vi.fn().mockImplementation(async () => ({ messageId: 'msg-123' }))
87
+ const send = vi.fn().mockResolvedValue({ messageId: 'msg-123' })
247
88
 
248
89
  const env = TestWorkflowEnvironment.create({ now: 0 })
249
90
 
250
91
  await env.execute(workflow, input, {
251
- claims: [mockClaim(SmtpSendEmailClaim, { send: mockSend })],
252
- env: { SMTP_CONNECTION_STRING: 'smtp://user:pass@smtp.example.com:587' },
92
+ claims: [mockClaim(SmtpSendEmailClaim, { send })],
93
+ env: { SMTP_URL: 'smtp://user:pass@smtp.example.com:587' },
253
94
  })
254
95
 
255
- // `send` receives the normalized activity input (single `to` becomes an array, etc.)
256
- expect(mockSend.mock.calls[0][0]).toMatchObject({
257
- to: ['recipient@example.com'],
258
- subject: 'Test',
259
- html: '<p>Hello</p>',
260
- })
96
+ expect(send.mock.calls[0][0]).toMatchObject({ to: ['recipient@example.com'] })
261
97
  ```
262
98
 
263
- ---
264
-
265
- ## Implementing SmtpSendEmailClaim
266
-
267
- 1. Subclass `SmtpSendEmailClaim` and implement **`send(input: SmtpSendEmailActivityInput)`**.
268
- 2. Read `connectionString`, `from`, `to`, `cc`, `bcc`, `subject`, `html`, `attachments` from `input` (arrays already normalized).
269
- 3. Return `{ messageId: string }` (or a Promise thereof). `messageId` is what workflows expose via `SmtpSendEmailOutput.messageId`.
270
- 4. For attachments, consume `Binary` via `.stream()`, `.contentType`, etc.
271
- 5. Register: `{ provide: SmtpSendEmailClaim, value: new MyClaim() }`.
272
-
273
- ---
274
-
275
- ## Checklist for agents
276
-
277
- 1. In workflows, use **`Smtp` + `smtp.send()`** (or `History.reconciler` with the exported `SmtpSendEmailActivity`).
278
- 2. Ensure runtime provides **`SmtpSendEmailClaim`** (or a mock in tests).
279
- 3. Set default **`from`** on `new Smtp(connectionString, { from })` or per-send; per-send wins.
280
- 4. Test workflows with **`TestWorkflowEnvironment`** + **`mockClaim(SmtpSendEmailClaim, { send })`**; assert on the mock's `mock.calls`.
281
- 5. For core workflow/reconciler concepts, use the **framework-core** skill (`@t4h.framework/core`).
99
+ ## Reference
282
100
 
283
- See also: `packages/smtp/README.md`; **framework-workflow-testing** for the test harness.
101
+ - Guide: https://tech4humans-brasil.github.io/framework/smtp/
102
+ - Secrets: https://tech4humans-brasil.github.io/framework/core/environment
103
+ - Workflow testing: https://tech4humans-brasil.github.io/framework/core/testing
104
+ - Types: `node_modules/@t4h.framework/smtp/dist/smtp.d.ts`
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # @t4h.framework/smtp
2
2
 
3
+ ## 0.7.9
4
+
5
+ ### Patch Changes
6
+
7
+ - [#276](https://github.com/tech4humans-brasil/framework/pull/276) [`7b38934`](https://github.com/tech4humans-brasil/framework/commit/7b38934e8f9207c98c33afcd76e3a7a4da7e8b14) Thanks [@gusteycamargo](https://github.com/gusteycamargo)! - The `framework-smtp` skill creates the `Smtp` client in the workflow body. A
8
+ read of `environments` at the top of the module throws `No env storage`. The
9
+ test sample imports `expect` and `vi` from `vitest`.
10
+
11
+ ## 0.7.8
12
+
13
+ ## 0.7.7
14
+
15
+ ### Patch Changes
16
+
17
+ - [#257](https://github.com/tech4humans-brasil/framework/pull/257) [`586bfb4`](https://github.com/tech4humans-brasil/framework/commit/586bfb4663f4a478527ee0c8db360603f034d615) Thanks [@gusteycamargo](https://github.com/gusteycamargo)! - The smtp README is now short and links to the site. The docs page now has the testing and runtime sections, and the README no longer names a `sendEmail` claim method: the method is `send`.
18
+
3
19
  ## 0.7.6
4
20
 
5
21
  ### Patch Changes
package/README.md CHANGED
@@ -1,47 +1,20 @@
1
1
  # @t4h.framework/smtp
2
2
 
3
- SMTP activity, claim, and a high-level client for sending emails within workflows.
3
+ Sends email from a T4H Framework workflow. Each send is a durable step, so a
4
+ replay does not send the message again.
4
5
 
5
- ## Claim
6
+ ## Install
6
7
 
7
- ### SmtpSendEmailClaim
8
-
9
- Abstract dependency for sending emails via SMTP. The runtime must provide a concrete implementation.
10
-
11
- ```typescript
12
- import { SmtpSendEmailClaim } from '@t4h.framework/smtp'
13
-
14
- // the runtime provides this:
15
- // { provide: SmtpSendEmailClaim, value: new MySmtpSendEmailClaimImpl() }
16
- ```
17
-
18
- ### Interface
19
-
20
- ```typescript
21
- abstract class SmtpSendEmailClaim {
22
- public abstract sendEmail(
23
- input: SmtpSendEmailActivityInput,
24
- ): ISmtpSendEmailOutput | Promise<ISmtpSendEmailOutput>
25
- }
26
- ```
27
-
28
- ## Activity
29
-
30
- ### SmtpSendEmailActivity
31
-
32
- A `SyncActivity` that sends an email and returns a `SmtpSendEmailOutput`. Internally it uses `SmtpSendEmailClaim` to perform the delivery.
33
-
34
- ```typescript
35
- import { SmtpSendEmailActivity } from '@t4h.framework/smtp'
8
+ ```bash
9
+ npm install @t4h.framework/smtp
36
10
  ```
37
11
 
38
- ## Smtp
12
+ Peer dependency: `@t4h.framework/core`.
39
13
 
40
- High-level SMTP client that wraps `SmtpSendEmailActivity` behind a simple API. Instantiated with a connection string and optional defaults.
14
+ ## Usage
41
15
 
42
- ### Basic usage
43
-
44
- The connection string `smtp://user:pass@host:587` carries the password, so the constructor takes a `Secret` and `tsc` rejects a string. Declare it with `type: Secret` in the `EnvDesign` in `src/constants/environments.ts`:
16
+ The connection string carries the password, so declare it with
17
+ `type: Secret` in the `EnvDesign` in `src/constants/environments.ts`:
45
18
 
46
19
  ```typescript
47
20
  // src/constants/environments.ts
@@ -53,67 +26,27 @@ export const environments = EnvDesign({
53
26
  ```
54
27
 
55
28
  ```typescript
29
+ import { Workflow } from '@t4h.framework/core'
56
30
  import { Smtp } from '@t4h.framework/smtp'
57
31
 
58
32
  import { environments } from './constants/environments.js'
59
33
 
60
- const smtp = new Smtp(environments.SMTP_URL)
61
-
62
- const result = await smtp.send({
63
- from: 'noreply@example.com',
64
- to: 'recipient@example.com',
65
- subject: 'Hello',
66
- html: '<p>Hello world</p>',
67
- })
68
-
69
- result.messageId // string
70
- ```
71
-
72
- ### Default sender
73
-
74
- ```typescript
75
- const smtp = new Smtp(environments.SMTP_URL, {
76
- from: 'noreply@example.com',
77
- })
78
-
79
- const result = await smtp.send({
80
- to: 'recipient@example.com',
81
- subject: 'Hello',
82
- html: '<p>Hello world</p>',
83
- })
84
- ```
85
-
86
- ### Multiple recipients, cc, and bcc
34
+ const notify = new Workflow({ id: 'notify' }, async () => {
35
+ const smtp = new Smtp(environments.SMTP_URL, { from: 'noreply@example.com' })
87
36
 
88
- ```typescript
89
- const result = await smtp.send({
90
- to: ['alice@example.com', 'bob@example.com'],
91
- cc: 'manager@example.com',
92
- bcc: ['audit@example.com'],
93
- subject: 'Team Update',
94
- html: '<p>Weekly report attached.</p>',
95
- })
96
- ```
37
+ const result = await smtp.send({
38
+ to: 'recipient@example.com',
39
+ subject: 'Hello',
40
+ html: '<p>Hello world</p>',
41
+ })
97
42
 
98
- ### Attachments
99
-
100
- ```typescript
101
- import type { Binary } from '@t4h.framework/core'
102
-
103
- const result = await smtp.send({
104
- to: 'recipient@example.com',
105
- subject: 'Report',
106
- html: '<p>See attached.</p>',
107
- attachments: [attachment],
43
+ return { messageId: result.messageId }
108
44
  })
109
45
  ```
110
46
 
111
- ## Output
112
-
113
- The `SmtpSendEmailOutput` object provides access to the sent message identifier:
47
+ Create the `Smtp` client in the workflow body. A test that imports the module
48
+ has no env, so a key read at module top level throws `No env storage`.
114
49
 
115
- ```typescript
116
- const result = await smtp.send({ ... })
50
+ ## Documentation
117
51
 
118
- result.messageId // string — the message ID returned by the SMTP server
119
- ```
52
+ https://tech4humans-brasil.github.io/framework/smtp/
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@t4h.framework/smtp",
3
- "version": "0.7.6",
3
+ "version": "0.7.9",
4
4
  "description": "SMTP module for the T4H Framework",
5
5
  "homepage": "https://github.com/tech4humans-brasil/framework/tree/main/packages/smtp",
6
6
  "bugs": "https://github.com/tech4humans-brasil/framework/issues",
@@ -33,7 +33,7 @@
33
33
  "test:watch": "vitest"
34
34
  },
35
35
  "devDependencies": {
36
- "@t4h.framework/core": "^0.20.0",
36
+ "@t4h.framework/core": "^0.21.2",
37
37
  "typescript": "^5.9.3",
38
38
  "vitest": "^4.0.18"
39
39
  },