@nocobase/plugin-workflow-mailer 2.1.0-beta.37 → 2.1.0-beta.38

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.
@@ -11,9 +11,9 @@ module.exports = {
11
11
  "@ant-design/icons": "5.6.1",
12
12
  "@formily/antd-v5": "1.2.3",
13
13
  "react": "18.2.0",
14
- "@nocobase/client": "2.1.0-beta.37",
15
- "@nocobase/plugin-workflow": "2.1.0-beta.37",
14
+ "@nocobase/client": "2.1.0-beta.38",
15
+ "@nocobase/plugin-workflow": "2.1.0-beta.38",
16
16
  "react-i18next": "11.18.6",
17
17
  "lodash": "4.18.1",
18
- "@nocobase/server": "2.1.0-beta.37"
18
+ "@nocobase/server": "2.1.0-beta.38"
19
19
  };
@@ -1 +1 @@
1
- {"name":"joi","description":"Object schema validation","version":"17.13.3","repository":"git://github.com/hapijs/joi","main":"lib/index.js","types":"lib/index.d.ts","browser":"dist/joi-browser.min.js","files":["lib/**/*","dist/*"],"keywords":["schema","validation"],"dependencies":{"@hapi/hoek":"^9.3.0","@hapi/topo":"^5.1.0","@sideway/address":"^4.1.5","@sideway/formula":"^3.0.1","@sideway/pinpoint":"^2.0.0"},"devDependencies":{"@hapi/bourne":"2.x.x","@hapi/code":"8.x.x","@hapi/joi-legacy-test":"npm:@hapi/joi@15.x.x","@hapi/lab":"^25.1.3","@types/node":"^14.18.63","typescript":"4.3.x"},"scripts":{"prepublishOnly":"cd browser && npm install && npm run build","test":"lab -t 100 -a @hapi/code -L -Y","test-cov-html":"lab -r html -o coverage.html -a @hapi/code"},"license":"BSD-3-Clause","_lastModified":"2026-05-26T00:46:20.438Z"}
1
+ {"name":"joi","description":"Object schema validation","version":"17.13.3","repository":"git://github.com/hapijs/joi","main":"lib/index.js","types":"lib/index.d.ts","browser":"dist/joi-browser.min.js","files":["lib/**/*","dist/*"],"keywords":["schema","validation"],"dependencies":{"@hapi/hoek":"^9.3.0","@hapi/topo":"^5.1.0","@sideway/address":"^4.1.5","@sideway/formula":"^3.0.1","@sideway/pinpoint":"^2.0.0"},"devDependencies":{"@hapi/bourne":"2.x.x","@hapi/code":"8.x.x","@hapi/joi-legacy-test":"npm:@hapi/joi@15.x.x","@hapi/lab":"^25.1.3","@types/node":"^14.18.63","typescript":"4.3.x"},"scripts":{"prepublishOnly":"cd browser && npm install && npm run build","test":"lab -t 100 -a @hapi/code -L -Y","test-cov-html":"lab -r html -o coverage.html -a @hapi/code"},"license":"BSD-3-Clause","_lastModified":"2026-05-29T02:54:07.786Z"}
@@ -1 +1 @@
1
- {"name":"nodemailer","version":"6.9.13","description":"Easy as cake e-mail sending from your Node.js applications","main":"lib/nodemailer.js","scripts":{"test":"node --test --test-concurrency=1 test/**/*.test.js test/**/*-test.js","test:coverage":"c8 node --test --test-concurrency=1 test/**/*.test.js test/**/*-test.js","lint":"eslint .","update":"rm -rf node_modules/ package-lock.json && ncu -u && npm install"},"repository":{"type":"git","url":"https://github.com/nodemailer/nodemailer.git"},"keywords":["Nodemailer"],"author":"Andris Reinman","license":"MIT-0","bugs":{"url":"https://github.com/nodemailer/nodemailer/issues"},"homepage":"https://nodemailer.com/","devDependencies":{"@aws-sdk/client-ses":"3.529.1","bunyan":"1.8.15","c8":"9.1.0","eslint":"8.57.0","eslint-config-nodemailer":"1.2.0","eslint-config-prettier":"9.1.0","libbase64":"1.3.0","libmime":"5.3.4","libqp":"2.1.0","nodemailer-ntlm-auth":"1.0.4","proxy":"1.0.2","proxy-test-server":"1.0.0","smtp-server":"3.13.3"},"engines":{"node":">=6.0.0"},"_lastModified":"2026-05-26T00:46:21.172Z"}
1
+ {"name":"nodemailer","version":"6.9.13","description":"Easy as cake e-mail sending from your Node.js applications","main":"lib/nodemailer.js","scripts":{"test":"node --test --test-concurrency=1 test/**/*.test.js test/**/*-test.js","test:coverage":"c8 node --test --test-concurrency=1 test/**/*.test.js test/**/*-test.js","lint":"eslint .","update":"rm -rf node_modules/ package-lock.json && ncu -u && npm install"},"repository":{"type":"git","url":"https://github.com/nodemailer/nodemailer.git"},"keywords":["Nodemailer"],"author":"Andris Reinman","license":"MIT-0","bugs":{"url":"https://github.com/nodemailer/nodemailer/issues"},"homepage":"https://nodemailer.com/","devDependencies":{"@aws-sdk/client-ses":"3.529.1","bunyan":"1.8.15","c8":"9.1.0","eslint":"8.57.0","eslint-config-nodemailer":"1.2.0","eslint-config-prettier":"9.1.0","libbase64":"1.3.0","libmime":"5.3.4","libqp":"2.1.0","nodemailer-ntlm-auth":"1.0.4","proxy":"1.0.2","proxy-test-server":"1.0.0","smtp-server":"3.13.3"},"engines":{"node":">=6.0.0"},"_lastModified":"2026-05-29T02:54:08.453Z"}
@@ -7,18 +7,11 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
  import Joi from 'joi';
10
- import { FlowNodeModel, Instruction, Processor } from '@nocobase/plugin-workflow';
10
+ import { FlowNodeModel, Instruction, InstructionResult, JobModel, Processor } from '@nocobase/plugin-workflow';
11
11
  export default class MailerInstruction extends Instruction {
12
12
  configSchema: Joi.ObjectSchema<any>;
13
- private static transporterMap;
14
- private static configMap;
15
- private getTransporterKey;
16
- private isConfigChanged;
17
- private createNewTransporter;
18
- private getTransporter;
19
- run(node: FlowNodeModel, prevJob: any, processor: Processor): Promise<{
20
- status: 1 | -1;
21
- result: any;
22
- }>;
23
- resume(node: FlowNodeModel, job: any, processor: Processor): Promise<any>;
13
+ run(node: FlowNodeModel, prevJob: JobModel, processor: Processor, options?: {
14
+ signal?: AbortSignal;
15
+ }): Promise<InstructionResult>;
16
+ resume(node: FlowNodeModel, job: JobModel, processor: Processor): Promise<JobModel>;
24
17
  }
@@ -40,10 +40,127 @@ __export(MailerInstruction_exports, {
40
40
  });
41
41
  module.exports = __toCommonJS(MailerInstruction_exports);
42
42
  var import_joi = __toESM(require("joi"));
43
- var import_util = require("util");
44
43
  var import_nodemailer = __toESM(require("nodemailer"));
45
44
  var import_plugin_workflow = require("@nocobase/plugin-workflow");
46
45
  var import_get = __toESM(require("lodash/get"));
46
+ const transporterMap = /* @__PURE__ */ new Map();
47
+ const configMap = /* @__PURE__ */ new Map();
48
+ function getTransporterKey(provider) {
49
+ const { host, port, auth } = provider;
50
+ return `${host}:${port}:${auth == null ? void 0 : auth.user}`;
51
+ }
52
+ function isConfigChanged(oldConfig, newConfig) {
53
+ const fields = ["host", "port", "secure", "auth.user", "auth.pass"];
54
+ return fields.some((key) => (0, import_get.default)(oldConfig, key) !== (0, import_get.default)(newConfig, key));
55
+ }
56
+ function createNewTransporter(key, config) {
57
+ const transporter = import_nodemailer.default.createTransport(config);
58
+ transporterMap.set(key, transporter);
59
+ configMap.set(key, config);
60
+ return transporter;
61
+ }
62
+ function getTransporter(provider) {
63
+ const key = getTransporterKey(provider);
64
+ const newConfig = provider;
65
+ const oldConfig = configMap.get(key);
66
+ if (!oldConfig) {
67
+ return createNewTransporter(key, newConfig);
68
+ }
69
+ if (isConfigChanged(oldConfig, newConfig)) {
70
+ const oldTransporter = transporterMap.get(key);
71
+ if (oldTransporter) {
72
+ oldTransporter.close();
73
+ }
74
+ return createNewTransporter(key, newConfig);
75
+ }
76
+ return transporterMap.get(key);
77
+ }
78
+ function discardTransporter(key, transporter) {
79
+ transporter.close();
80
+ if (transporterMap.get(key) === transporter) {
81
+ transporterMap.delete(key);
82
+ configMap.delete(key);
83
+ }
84
+ }
85
+ function createAbortSignal(processor, signal, timeout = 0) {
86
+ var _a, _b, _c, _d, _e, _f, _g;
87
+ const abortController = new AbortController();
88
+ let timeoutTimer = null;
89
+ let abortListener = null;
90
+ const abort = (reason) => {
91
+ if (abortController.signal.aborted) {
92
+ return;
93
+ }
94
+ abortController.abort(reason instanceof Error ? reason : new import_plugin_workflow.WorkflowTimeoutError());
95
+ };
96
+ if (signal == null ? void 0 : signal.aborted) {
97
+ abort(signal.reason);
98
+ } else {
99
+ abortListener = () => abort(signal == null ? void 0 : signal.reason);
100
+ signal == null ? void 0 : signal.addEventListener("abort", abortListener, { once: true });
101
+ }
102
+ const executionWorkflow = processor.execution.workflow ?? ((_b = (_a = processor.execution).get) == null ? void 0 : _b.call(_a, "workflow"));
103
+ const executionWorkflowOptions = (executionWorkflow == null ? void 0 : executionWorkflow.options) ?? ((_c = executionWorkflow == null ? void 0 : executionWorkflow.get) == null ? void 0 : _c.call(executionWorkflow, "options")) ?? {};
104
+ const workflowTimeout = Number(timeout || executionWorkflowOptions.timeout || 0);
105
+ const expiresAt = processor.execution.expiresAt ?? ((_e = (_d = processor.execution).get) == null ? void 0 : _e.call(_d, "expiresAt"));
106
+ const startedAt = processor.execution.startedAt ?? ((_g = (_f = processor.execution).get) == null ? void 0 : _g.call(_f, "startedAt"));
107
+ const remaining = expiresAt ? new Date(expiresAt).getTime() - Date.now() : workflowTimeout > 0 && startedAt ? new Date(startedAt).getTime() + workflowTimeout - Date.now() : workflowTimeout > 0 ? workflowTimeout : null;
108
+ if (remaining != null) {
109
+ if (remaining <= 0) {
110
+ abort(new import_plugin_workflow.WorkflowTimeoutError());
111
+ } else {
112
+ timeoutTimer = setTimeout(() => abort(new import_plugin_workflow.WorkflowTimeoutError()), remaining);
113
+ }
114
+ }
115
+ return {
116
+ signal: abortController.signal,
117
+ cleanup: () => {
118
+ if (timeoutTimer) {
119
+ clearTimeout(timeoutTimer);
120
+ }
121
+ if (signal && abortListener) {
122
+ signal.removeEventListener("abort", abortListener);
123
+ }
124
+ }
125
+ };
126
+ }
127
+ function sendMail(transporter, transporterKey, payload, signal) {
128
+ if (signal == null ? void 0 : signal.aborted) {
129
+ discardTransporter(transporterKey, transporter);
130
+ return Promise.reject(signal.reason);
131
+ }
132
+ return new Promise((resolve, reject) => {
133
+ let finished = false;
134
+ const cleanup = () => {
135
+ signal == null ? void 0 : signal.removeEventListener("abort", onAbort);
136
+ };
137
+ const done = (error, result) => {
138
+ if (finished) {
139
+ return;
140
+ }
141
+ finished = true;
142
+ cleanup();
143
+ if (error) {
144
+ reject(error);
145
+ } else {
146
+ resolve(result);
147
+ }
148
+ };
149
+ const onAbort = () => {
150
+ if (finished) {
151
+ return;
152
+ }
153
+ discardTransporter(transporterKey, transporter);
154
+ done((signal == null ? void 0 : signal.reason) instanceof Error ? signal.reason : new import_plugin_workflow.WorkflowTimeoutError());
155
+ };
156
+ signal == null ? void 0 : signal.addEventListener("abort", onAbort, { once: true });
157
+ try {
158
+ transporter.sendMail(payload, done);
159
+ } catch (error) {
160
+ done(error);
161
+ }
162
+ });
163
+ }
47
164
  class MailerInstruction extends import_plugin_workflow.Instruction {
48
165
  configSchema = import_joi.default.object({
49
166
  provider: import_joi.default.object({
@@ -65,39 +182,8 @@ class MailerInstruction extends import_plugin_workflow.Instruction {
65
182
  text: import_joi.default.string(),
66
183
  ignoreFail: import_joi.default.boolean().default(false)
67
184
  });
68
- static transporterMap = /* @__PURE__ */ new Map();
69
- static configMap = /* @__PURE__ */ new Map();
70
- getTransporterKey(provider) {
71
- const { host, port, auth } = provider;
72
- return `${host}:${port}:${auth == null ? void 0 : auth.user}`;
73
- }
74
- isConfigChanged(oldConfig, newConfig) {
75
- const fields = ["host", "port", "secure", "auth.user", "auth.pass"];
76
- return fields.some((key) => (0, import_get.default)(oldConfig, key) !== (0, import_get.default)(newConfig, key));
77
- }
78
- createNewTransporter(key, config) {
79
- const transporter = import_nodemailer.default.createTransport(config);
80
- MailerInstruction.transporterMap.set(key, transporter);
81
- MailerInstruction.configMap.set(key, config);
82
- return transporter;
83
- }
84
- getTransporter(provider) {
85
- const key = this.getTransporterKey(provider);
86
- const newConfig = provider;
87
- const oldConfig = MailerInstruction.configMap.get(key);
88
- if (!oldConfig) {
89
- return this.createNewTransporter(key, newConfig);
90
- }
91
- if (this.isConfigChanged(oldConfig, newConfig)) {
92
- const oldTransporter = MailerInstruction.transporterMap.get(key);
93
- if (oldTransporter) {
94
- oldTransporter.close();
95
- }
96
- return this.createNewTransporter(key, newConfig);
97
- }
98
- return MailerInstruction.transporterMap.get(key);
99
- }
100
- async run(node, prevJob, processor) {
185
+ async run(node, prevJob, processor, options) {
186
+ var _a, _b;
101
187
  const {
102
188
  provider,
103
189
  contentType,
@@ -108,32 +194,41 @@ class MailerInstruction extends import_plugin_workflow.Instruction {
108
194
  html,
109
195
  text,
110
196
  ignoreFail,
111
- ...options
197
+ ...others
112
198
  } = processor.getParsedValue(node.config, node.id);
113
199
  const { workflow } = processor.execution;
114
- const sync = this.workflow.isWorkflowSync(workflow);
115
- const transporter = this.getTransporter(provider);
116
- const send = (0, import_util.promisify)(transporter.sendMail.bind(transporter));
200
+ const currentWorkflow = (workflow == null ? void 0 : workflow.options) || ((_a = workflow == null ? void 0 : workflow.get) == null ? void 0 : _a.call(workflow, "options")) ? workflow : await node.getWorkflow();
201
+ const sync = this.workflow.isWorkflowSync(currentWorkflow);
202
+ const workflowOptions = (currentWorkflow == null ? void 0 : currentWorkflow.options) ?? ((_b = currentWorkflow == null ? void 0 : currentWorkflow.get) == null ? void 0 : _b.call(currentWorkflow, "options")) ?? {};
203
+ const workflowTimeout = Number(workflowOptions.timeout ?? 0);
204
+ const transporterKey = getTransporterKey(provider);
205
+ const transporter = getTransporter(provider);
206
+ const mailAbort = createAbortSignal(processor, options == null ? void 0 : options.signal, workflowTimeout);
117
207
  const payload = {
118
- ...options,
208
+ ...others,
119
209
  ...contentType === "html" ? { html } : { text },
120
210
  subject: subject == null ? void 0 : subject.trim(),
121
211
  to: to ? to.flat().map((item) => item == null ? void 0 : item.trim()).filter(Boolean) : void 0,
122
- cc: cc ? cc.flat().map((item) => item == null ? void 0 : item.trim()).filter(Boolean) : null,
123
- bcc: bcc ? bcc.flat().map((item) => item == null ? void 0 : item.trim()).filter(Boolean) : null
212
+ cc: cc ? cc.flat().map((item) => item == null ? void 0 : item.trim()).filter(Boolean) : [],
213
+ bcc: bcc ? bcc.flat().map((item) => item == null ? void 0 : item.trim()).filter(Boolean) : []
124
214
  };
125
215
  if (sync) {
126
216
  try {
127
- const result = await send(payload);
217
+ const result = await sendMail(transporter, transporterKey, payload, mailAbort.signal);
128
218
  return {
129
219
  status: import_plugin_workflow.JOB_STATUS.RESOLVED,
130
220
  result
131
221
  };
132
222
  } catch (error) {
223
+ if (mailAbort.signal.aborted) {
224
+ throw error;
225
+ }
133
226
  return {
134
227
  status: ignoreFail ? import_plugin_workflow.JOB_STATUS.RESOLVED : import_plugin_workflow.JOB_STATUS.FAILED,
135
228
  result: error
136
229
  };
230
+ } finally {
231
+ mailAbort.cleanup();
137
232
  }
138
233
  }
139
234
  const { id } = processor.saveJob({
@@ -145,26 +240,40 @@ class MailerInstruction extends import_plugin_workflow.Instruction {
145
240
  await processor.exit();
146
241
  const jobDone = { status: import_plugin_workflow.JOB_STATUS.PENDING };
147
242
  try {
148
- const response = await send(payload);
243
+ const response = await sendMail(transporter, transporterKey, payload, mailAbort.signal);
149
244
  processor.logger.info(`smtp-mailer (#${node.id}) sent successfully.`);
150
245
  jobDone.status = import_plugin_workflow.JOB_STATUS.RESOLVED;
151
246
  jobDone.result = response;
152
247
  } catch (error) {
153
248
  processor.logger.warn(`smtp-mailer (#${node.id}) sent failed: ${error.message}`);
154
- jobDone.status = import_plugin_workflow.JOB_STATUS.FAILED;
249
+ jobDone.status = mailAbort.signal.aborted ? import_plugin_workflow.JOB_STATUS.ABORTED : import_plugin_workflow.JOB_STATUS.FAILED;
155
250
  jobDone.result = error;
156
251
  } finally {
252
+ mailAbort.cleanup();
157
253
  processor.logger.debug(`smtp-mailer (#${node.id}) sending ended, resume workflow...`);
158
254
  const job = await this.workflow.app.db.getRepository("jobs").findOne({
159
255
  filterByTk: id
160
256
  });
161
- job.set(jobDone);
162
- this.workflow.resume(job);
257
+ const execution = await job.getExecution();
258
+ const aborted = await this.workflow.abortExecutionIfExpired(execution);
259
+ if (!aborted) {
260
+ await execution.reload();
261
+ await job.reload();
262
+ }
263
+ if (!aborted && execution.status === import_plugin_workflow.EXECUTION_STATUS.STARTED && job.status === import_plugin_workflow.JOB_STATUS.PENDING) {
264
+ job.set(jobDone);
265
+ job.execution = execution;
266
+ this.workflow.resume(job);
267
+ } else {
268
+ processor.logger.warn(
269
+ `smtp-mailer (#${node.id}) result discarded because execution (${execution.id}) is ended`
270
+ );
271
+ }
163
272
  }
164
273
  }
165
274
  async resume(node, job, processor) {
166
275
  const { ignoreFail } = node.config;
167
- if (ignoreFail) {
276
+ if (ignoreFail && job.status !== import_plugin_workflow.JOB_STATUS.ABORTED) {
168
277
  job.set("status", import_plugin_workflow.JOB_STATUS.RESOLVED);
169
278
  }
170
279
  return job;
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "description": "Send email in workflow.",
7
7
  "description.ru-RU": "Отправляет электронное письмо в рамках рабочего процесса.",
8
8
  "description.zh-CN": "可用于在工作流中发送电子邮件。",
9
- "version": "2.1.0-beta.37",
9
+ "version": "2.1.0-beta.38",
10
10
  "license": "Apache-2.0",
11
11
  "main": "./dist/server/index.js",
12
12
  "homepage": "https://docs.nocobase.com/handbook/workflow-smtp-mailer",
@@ -25,7 +25,7 @@
25
25
  "@nocobase/server": "2.x",
26
26
  "@nocobase/test": "2.x"
27
27
  },
28
- "gitHead": "7132e5b83ecc0e42b54715eaf1429c72bcef34ae",
28
+ "gitHead": "d1c585108ff6e51c17b0b52bacb1a2d621d9c119",
29
29
  "keywords": [
30
30
  "NocoBase",
31
31
  "Workflow",