data-primals-engine 1.2.2 → 1.2.3

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.
@@ -8,7 +8,7 @@ import ivm from 'isolated-vm';
8
8
 
9
9
  import {Logger} from "../gameObject.js";
10
10
  import {deleteData, insertData, patchData, searchData} from "./data.js";
11
- import {maxExecutionsByStep, maxWorkflowSteps, timeoutVM} from "../constants.js";
11
+ import {emailDefaultConfig, maxExecutionsByStep, maxWorkflowSteps, timeoutVM} from "../constants.js";
12
12
  import {ChatOpenAI} from "@langchain/openai";
13
13
  import {ChatGoogleGenerativeAI} from "@langchain/google-genai";
14
14
  import {ChatPromptTemplate} from "@langchain/core/prompts";
@@ -16,6 +16,8 @@ import i18n from "../../src/i18n.js";
16
16
  import {sendEmail} from "../email.js";
17
17
 
18
18
  import * as workflowModule from './workflow.js';
19
+ import {isConditionMet} from "../filter.js";
20
+ import util from "node:util";
19
21
 
20
22
  let logger = null;
21
23
  export async function onInit(defaultEngine) {
@@ -180,6 +182,22 @@ export async function scheduleWorkflowTriggers() {
180
182
  }
181
183
  }
182
184
 
185
+ async function handleWaitAction(actionDef, contextData, user) {
186
+ const { duration, durationUnit } = actionDef;
187
+ if (!duration || !durationUnit) {
188
+ return { success: false, message: "Wait action requires 'duration' and 'durationUnit'." };
189
+ }
190
+
191
+ // Retourne un statut spécial que le moteur de workflow comprendra
192
+ return {
193
+ success: true,
194
+ status: 'paused', // Statut spécial
195
+ duration,
196
+ durationUnit,
197
+ message: `Workflow will be paused for ${duration} ${durationUnit}.`
198
+ };
199
+ }
200
+
183
201
 
184
202
  export async function executeSafeJavascript(actionDef, context, user) {
185
203
  const code = actionDef.script;
@@ -190,73 +208,102 @@ export async function executeSafeJavascript(actionDef, context, user) {
190
208
  const vmContext = await isolate.createContext();
191
209
  const jail = vmContext.global;
192
210
 
193
- // Helper to create a secure reference for our API functions
194
- const createJailFunction = (fn) => new ivm.Reference(fn);
211
+ const find = async (modelName, filter) => {
212
+ const result = await searchData({ model: modelName, filter: JSON.parse(filter) }, user);
213
+ return new ivm.ExternalCopy(result).copyInto();
214
+ };
215
+ const findOne = async (modelName, filter) => {
216
+ const result = await searchData({ model: modelName, filter: JSON.parse(filter), limit: 1 }, user);
217
+ return new ivm.ExternalCopy(result.data?.[0] || null).copyInto();
218
+ };
195
219
 
196
220
  // 1. Build the sandboxed API
197
- await jail.set('db', createJailFunction({
198
- create: (modelName, dataObject) => insertData(modelName, dataObject, {}, user),
199
- find: async (modelName, filter) => {
200
- const result = await searchData({ model: modelName, filter }, user);
201
- return new ivm.ExternalCopy(result.data).copyInto();
202
- },
203
- findOne: async (modelName, filter) => {
204
- const result = await searchData({ model: modelName, filter, limit: 1 }, user);
205
- return new ivm.ExternalCopy(result.data?.[0] || null).copyInto();
206
- },
207
- update: (modelName, filter, updateObject) => patchData(modelName, filter, updateObject, {}, user),
208
- delete: (modelName, filter) => deleteData(modelName, filter, user)
209
- }));
221
+ await jail.set('_db_create', new ivm.Reference((modelName, dataObject) => insertData(modelName, JSON.parse(dataObject), {}, user, false)));
222
+ await jail.set('_db_find', new ivm.Reference(find));
223
+ await jail.set('_db_findOne', new ivm.Reference(findOne));
224
+
225
+ await jail.set('_db_update', new ivm.Reference((modelName, filter, updateObject) => patchData(modelName, JSON.parse(filter), JSON.parse(updateObject), {}, user, false)));
226
+ await jail.set('_db_delete', new ivm.Reference((modelName, filter) => deleteData(modelName, JSON.parse(filter), user, false)));
210
227
 
211
- const createLoggerFunction = (level) => {
228
+ const createLoggerMethod = (level) => {
212
229
  return (...args) => {
213
- const finalMessage = logger.trace(level, '[VM Script]', ...args);
230
+ const message = args.join(' ');
214
231
  collectedLogs.push({
215
- level: level,
216
- message: finalMessage,
232
+ level,
233
+ message,
217
234
  timestamp: new Date().toISOString()
218
235
  });
236
+ logger.trace(level, '[VM Script]', message);
219
237
  };
220
238
  };
221
239
 
222
- await jail.set('logger', createJailFunction({
223
- info: createLoggerFunction('info'),
224
- warn: createLoggerFunction('warn'),
225
- error: createLoggerFunction('error')
240
+ await jail.set('_log_info', createLoggerMethod('info'));
241
+ await jail.set('_log_warn', createLoggerMethod('warn'));
242
+ await jail.set('_log_error', createLoggerMethod('error'));
243
+ await jail.set('_env_get', new ivm.Reference(async (variableName) => {
244
+ if (!variableName) return null;
245
+ const result = await searchData({ model: 'env', filter: { name: variableName }, limit: 1 }, user);
246
+ return new ivm.ExternalCopy(result.data?.[0]?.value || null).copyInto();
226
247
  }));
227
-
228
- await jail.set('env', createJailFunction({
229
- get: async (variableName) => {
230
- if (!variableName) return null;
231
- const result = await searchData({ model: 'env', filter: { name: variableName }, limit: 1 }, user);
232
- return new ivm.ExternalCopy(result.data?.[0]?.value || null).copyInto();
233
- },
234
- getAll: async () => {
235
- const result = await searchData({ model: 'env' }, user);
236
- const envObject = result.data.reduce((acc, v) => {
237
- acc[v.name] = v.value;
238
- return acc;
239
- }, {});
240
- return new ivm.ExternalCopy(envObject).copyInto();
241
- }
248
+ await jail.set('_env_get_all', new ivm.Reference(async () => {
249
+ const result = await searchData({ model: 'env' }, user);
250
+ const envObject = result.data.reduce((acc, v) => {
251
+ acc[v.name] = v.value;
252
+ return acc;
253
+ }, {});
254
+ return new ivm.ExternalCopy(envObject).copyInto();
242
255
  }));
243
256
 
244
- // Inject the context data securely
245
- await jail.set('context', new ivm.ExternalCopy(context).copyInto());
257
+ // Contexte sécurisé
258
+ const safeContext = JSON.parse(JSON.stringify(context));
259
+ await jail.set('context', new ivm.ExternalCopy(safeContext).copyInto());
246
260
 
247
- // 2. Prepare and run the script
248
- // The script is wrapped in an async IIFE to handle promises correctly
261
+ // Exécution
249
262
  const fullScript = `
250
- (async () => {
263
+ const normalizeArgs = args => args.map(arg => {
264
+ if (typeof arg === 'object' && arg !== null) {
265
+ return JSON.stringify(arg); // Convert objects to strings
266
+ }
267
+ return arg;
268
+ });
269
+ const db = {
270
+ create: (...args) => _db_create.applySyncPromise(null, normalizeArgs(args)),
271
+ find: (...args) => _db_find.applySyncPromise(null, normalizeArgs(args)),
272
+ findOne: (...args) => _db_findOne.applySyncPromise(null, normalizeArgs(args)),
273
+ update: (...args) => _db_update.applySyncPromise(null, normalizeArgs(args)),
274
+ delete: (...args) => _db_delete.applySyncPromise(null, normalizeArgs(args))
275
+ };
276
+
277
+ const logger = {
278
+ info: _log_info,
279
+ warn: _log_warn,
280
+ error: _log_error
281
+ };
282
+
283
+ const env = {
284
+ get: _env_get,
285
+ getAll: _env_get_all
286
+ };
287
+
288
+ (async function() {
251
289
  ${code}
252
290
  })();
253
291
  `;
254
292
 
255
- const script = await isolate.compileScript(fullScript);
256
- const result = await script.run(vmContext, { timeout: timeoutVM, promise: true });
293
+ const TIMEOUT = 5000;
294
+ const script = await isolate.compileScript(fullScript, { timeout: TIMEOUT });
295
+ const result = await script.run(vmContext, {
296
+ timeout: TIMEOUT,
297
+ promise: true,
298
+ copy: true // Copie automatique du résultat
299
+ });
257
300
 
258
- // The result is returned as an ExternalCopy, we need to copy it out
259
- return { success: true, data: result, logs: collectedLogs };
301
+ return {
302
+ success: true,
303
+ data: result,
304
+ logs: collectedLogs,
305
+ updatedContext: { result }
306
+ };
260
307
 
261
308
  } catch (error) {
262
309
  const errorMessage = `Script execution failed: ${error.message}`;
@@ -485,7 +532,7 @@ async function handleCreateDataAction(actionDef, contextData, user, dbCollection
485
532
  logger.debug('Final data object after substitution:', dataObject);
486
533
 
487
534
  // 3. Appeler insertData avec l'objet correctement substitué
488
- const result = await insertData(targetModel, dataObject, [], user, true, true); // On attend la fin du workflow déclenché par cette création
535
+ const result = await insertData(targetModel, dataObject, [], user, false, true); // On attend la fin du workflow déclenché par cette création
489
536
 
490
537
  if (result.success) {
491
538
  return { success: true, insertedIds: result.insertedIds };
@@ -607,16 +654,7 @@ async function handleUpdateDataAction(actionDef, contextData, user) {
607
654
  selectorObject,
608
655
  updatesObject,
609
656
  {},
610
- user
611
- );
612
-
613
-
614
- console.log(
615
- targetModel,
616
- selectorObject,
617
- updatesObject,
618
- {},
619
- user
657
+ user, false
620
658
  );
621
659
 
622
660
  // 6. Return result
@@ -626,7 +664,10 @@ async function handleUpdateDataAction(actionDef, contextData, user) {
626
664
  success: true,
627
665
  modifiedCount: updateResult.modifiedCount,
628
666
  matchedCount: updateResult.matchedCount,
629
- message: updateResult.message
667
+ message: updateResult.message,
668
+ updatedContext: {
669
+ triggerData: {...contextData.triggerData || {}, ...updatesObject}
670
+ }
630
671
  };
631
672
  } else {
632
673
  // updateData now throws errors, so this 'else' might not be reached often,
@@ -765,6 +806,9 @@ export async function executeStepAction(actionDef, contextData, user, dbCollecti
765
806
  case 'SendEmail':
766
807
  result = await handleSendEmailAction(actionDef, contextData, user);
767
808
  break;
809
+ case 'Wait':
810
+ result = await handleWaitAction(actionDef, contextData, user);
811
+ break;
768
812
  case 'ExecuteScript':
769
813
  result = await executeSafeJavascript(actionDef, contextData, user);
770
814
  break;
@@ -968,6 +1012,9 @@ export async function substituteVariables(template, contextData, user) {
968
1012
  // 5. Logique de résolution de valeur améliorée avec resolvePathValue
969
1013
  const findValue = async (key) => {
970
1014
  let path = key.trim();
1015
+ if (path.startsWith('context.')) {
1016
+ path = path.substring('context.'.length);
1017
+ }
971
1018
  if (path.endsWith('._id')) {
972
1019
  const basePath = path.slice(0, -4);
973
1020
  const value = await findValue(basePath);
@@ -1012,7 +1059,20 @@ export async function substituteVariables(template, contextData, user) {
1012
1059
  if (singlePlaceholderMatch) {
1013
1060
  const key = singlePlaceholderMatch[1];
1014
1061
  const value = await findValue(key);
1015
- return value !== undefined ? value : template;
1062
+
1063
+ if (value === undefined) {
1064
+ return template; // Placeholder not found, return as is.
1065
+ }
1066
+
1067
+ // If the resolved value is a string, it might contain more placeholders.
1068
+ // We recursively call substituteVariables on it, but only if it's different
1069
+ // from the original template to prevent infinite loops.
1070
+ if (typeof value === 'string' && value !== template) {
1071
+ return substituteVariables(value, contextData, user);
1072
+ }
1073
+
1074
+ // For non-string values or if value is same as template, return the value.
1075
+ return value;
1016
1076
  }
1017
1077
 
1018
1078
  // CAS B : La chaîne contient plusieurs placeholders ou mix texte/variables
@@ -1107,17 +1167,16 @@ export async function triggerWorkflows(triggerData, user, eventType) {
1107
1167
  try {
1108
1168
  const finalFilter = {
1109
1169
  '$and': [
1170
+ {"_id": { "$toObjectId": triggerData._id.toString() }},
1110
1171
  dataFilterCondition // Applique la condition du trigger
1111
1172
  ]
1112
1173
  };
1113
1174
 
1114
- console.debug(`[Workflow Trigger] Vérification dataFilter pour trigger ${trigger._id} avec filtre combiné:`, JSON.stringify(finalFilter));
1115
-
1116
- // Exécuter la vérification dans la base de données
1117
- // Utilisation de countDocuments pour une vérification rapide
1118
- const matchCount = await searchData({ model: targetModelName, filter: finalFilter, limit: 1 }, user);
1119
1175
 
1120
- if (!matchCount.count) {
1176
+ console.debug(`[Workflow Trigger] Vérification dataFilter pour trigger ${trigger._id} avec filtre combiné:`, JSON.stringify(finalFilter));
1177
+ console.log({triggerData, finalFilter});
1178
+ const match = await searchData({ model: triggerData._model, filter: finalFilter, limit: 1 }, user);
1179
+ if (!match.count) {
1121
1180
  console.debug(`[Workflow Trigger] Trigger ${trigger._id}: dataFilter non satisfait par la donnée ${dataId}. WorkflowRun non créé.`);
1122
1181
  continue; // Passer au trigger suivant
1123
1182
  } else {
@@ -1277,9 +1336,16 @@ export async function processWorkflowRun(workflowRunId, user) {
1277
1336
  let conditionsMet = true;
1278
1337
 
1279
1338
  try {
1339
+ // Add logging to see the actual pipeline being executed
1340
+ logger.debug('Executing pipeline:', JSON.stringify(await substituteVariables(currentStepDef.conditions, contextData, user), null, 2));
1341
+
1342
+ // And log the context data to verify processedChunk exists
1343
+ logger.debug('Context data:', JSON.stringify(contextData, null, 2));
1344
+
1280
1345
  // --- 7. Évaluation des conditions de l'étape ---
1281
1346
  if (currentStepDef.conditions && Object.keys(currentStepDef.conditions).length > 0) {
1282
- const searchResult = await searchData({ model: contextData.triggerDataModel, filter: currentStepDef.conditions, limit: 1}, user);
1347
+ const substitutedConditions = await substituteVariables(currentStepDef.conditions, contextData, user);
1348
+ const searchResult = await searchData({ model: contextData.triggerDataModel, filter: substitutedConditions, limit: 1}, user);
1283
1349
  conditionsMet = searchResult && searchResult.count > 0;
1284
1350
  logger.info(`[processWorkflowRun] Run ID: ${runId}, Step ID: ${currentStepId}: Conditions evaluated. Found ${searchResult ? searchResult.count : 0} match(es). Result: ${conditionsMet}`);
1285
1351
  }
@@ -1292,7 +1358,41 @@ export async function processWorkflowRun(workflowRunId, user) {
1292
1358
  if (!isObjectId(actionId)) continue;
1293
1359
  const actionDef = await dbCollection.findOne({ _id: new ObjectId(actionId), _model: 'workflowAction' });
1294
1360
  if (!actionDef) return await logError(`Action definition ${actionId} not found.`);
1361
+ console.log({actionDef});
1295
1362
  const actionResult = await workflowModule.executeStepAction(actionDef, contextData, user, dbCollection);
1363
+
1364
+ if (actionResult.status === 'paused') {
1365
+ // L'action demande une pause !
1366
+ const { duration, durationUnit } = actionResult;
1367
+ const now = new Date();
1368
+ let resumeAt = new Date(now);
1369
+
1370
+ // Calculer la date de reprise
1371
+ const ms = { seconds: 1000, minutes: 60000, hours: 3600000, days: 86400000 };
1372
+ resumeAt.setTime(now.getTime() + (duration * ms[durationUnit]));
1373
+
1374
+ logger.info(`[processWorkflowRun] Run ID: ${runId} is pausing. Will resume at: ${resumeAt.toISOString()}`);
1375
+
1376
+ // Mettre à jour le workflowRun avec le statut 'paused' et la date de reprise
1377
+ await dbCollection.updateOne({ _id: runId }, {
1378
+ $set: {
1379
+ status: 'paused',
1380
+ currentStep: currentStepDef.onSuccessStep, // On prépare la prochaine étape
1381
+ contextData,
1382
+ log: actionResult.message
1383
+ }
1384
+ });
1385
+
1386
+ // Planifier le réveil du workflow
1387
+ schedule.scheduleJob(resumeAt, async () => {
1388
+ logger.info(`[Scheduler] Waking up paused workflowRun ID: ${runId}`);
1389
+ // On relance le traitement pour ce workflow spécifique
1390
+ await workflowModule.processWorkflowRun(runId, user);
1391
+ });
1392
+
1393
+ // Arrêter le traitement actuel de cette exécution
1394
+ return; // Très important de stopper la boucle ici
1395
+ }
1296
1396
  if (!actionResult.success) {
1297
1397
  stepSucceeded = false;
1298
1398
  logInfo = actionResult.message || `Action ${actionDef.name || actionId} failed.`;
@@ -1303,6 +1403,7 @@ export async function processWorkflowRun(workflowRunId, user) {
1303
1403
  if (actionResult.updatedContext) {
1304
1404
  contextData = { ...contextData, ...actionResult.updatedContext };
1305
1405
  }
1406
+ console.log("action", util.inspect(actionResult, false, 8, true));
1306
1407
  logger.info(`[processWorkflowRun] Run ID: ${runId}, Step ID: ${currentStepId}, Action ID: ${actionId}: Executed successfully.`);
1307
1408
  }
1308
1409
  }
@@ -1362,7 +1463,7 @@ export async function processWorkflowRun(workflowRunId, user) {
1362
1463
  logger.error(`[processWorkflowRun] Critical error during processing of workflowRun ID: ${runId}. Error: ${error.message}`, error.stack);
1363
1464
  await dbCollection.updateOne(
1364
1465
  { _id: runId, status: { $nin: ['completed', 'failed', 'cancelled'] } },
1365
- { $set: { status: 'failed', error: `Critical error: ${error.message}`, completedAt: new Date(), stepExecutionsCount } }
1466
+ { $set: { status: 'failed', log: `Critical error: ${error.message}`, completedAt: new Date(), stepExecutionsCount } }
1366
1467
  );
1367
1468
  }
1368
1469
  }
@@ -1466,57 +1567,122 @@ async function executeGenerateAIContentAction(action, context, user) {
1466
1567
 
1467
1568
 
1468
1569
  /**
1469
- * Gère l'action d'envoi d'email d'un workflow.
1470
- * @param {object} action - L'objet workflowAction.
1471
- * @param {object} triggerData - Les données qui ont déclenché le workflow.
1570
+ * Gère l'action d'envoi d'e-mail d'un workflow.
1571
+ * Cette version améliorée peut traiter une liste de destinataires, en envoyant un e-mail
1572
+ * individuel et personnalisé à chacun. Elle gère les placeholders dans le sujet et le corps
1573
+ * de l'e-mail en se basant sur le contexte de chaque destinataire.
1574
+ *
1575
+ * @param {object} action - La définition de l'action 'SendEmail'.
1576
+ * @param {object} contextData - Le contexte d'exécution actuel du workflow.
1472
1577
  * @param {object} user - L'utilisateur propriétaire du workflow.
1578
+ * @returns {Promise<{success: boolean, message: string, data?: {sent: string[], failed: any[]}}>}
1473
1579
  */
1474
- async function handleSendEmailAction(action, triggerData, user) {
1475
-
1476
- logger.info(`[Workflow] Exécution de l'action sendEmail pour l'utilisateur ${user.username}.`);
1580
+ async function handleSendEmailAction(action, contextData, user) {
1581
+ logger.info(`[handleSendEmailAction] Executing for user ${user.username}.`);
1477
1582
 
1478
1583
  // 1. Récupérer la configuration SMTP depuis le modèle 'env' de l'utilisateur
1479
1584
  const envVars = await searchData({
1480
- model: 'env', limit: 100 } // Limite raisonnable pour les variables d'env
1481
- , user);
1482
-
1483
- if (!envVars.data || envVars.data.length === 0) {
1484
- throw new Error("Aucune variable d'environnement (modèle 'env') trouvée pour la configuration SMTP.");
1485
- }
1585
+ model: 'env',
1586
+ filter: { $in: ['$name', ['SMTP_HOST', 'SMTP_PORT', 'SMTP_USER', 'SMTP_PASS', 'SMTP_FROM']] }
1587
+ }, user);
1486
1588
 
1487
1589
  const smtpConfig = envVars.data.reduce((acc, variable) => {
1488
- if (['SMTP_HOST', 'SMTP_PORT', 'SMTP_USER', 'SMTP_PASS', 'SMTP_FROM'].includes(variable.name)) {
1489
- acc[variable.name.replace('SMTP_', '').toLowerCase()] = variable.value;
1490
- }
1590
+ acc[variable.name.replace('SMTP_', '').toLowerCase()] = variable.value;
1491
1591
  return acc;
1492
1592
  }, {});
1593
+ if( !smtpConfig.port )
1594
+ smtpConfig.port = emailDefaultConfig.port;
1493
1595
 
1494
- // 2. Extraire la configuration de l'action et résoudre les placeholders
1596
+ // 2. Valider la configuration de l'action
1495
1597
  const { emailRecipients, emailSubject, emailContent } = action;
1496
1598
  if (!emailRecipients || !emailSubject || !emailContent) {
1497
- throw new Error("SendEmail incomplete (emailRecipients, emailSubject, emailContent are needed).");
1599
+ const msg = "SendEmail action is incomplete. 'emailRecipients', 'emailSubject', and 'emailContent' are required.";
1600
+ logger.error(`[handleSendEmailAction] ${msg}`);
1601
+ return { success: false, message: msg };
1498
1602
  }
1499
1603
 
1500
1604
  try {
1501
- const context = { data: triggerData, user }; // Contexte pour la résolution des placeholders
1605
+ // 3. Résoudre la liste des destinataires. Peut être un placeholder qui retourne un tableau.
1606
+ let resolvedRecipients = await substituteVariables(emailRecipients, contextData, user);
1607
+
1608
+ // S'assurer que nous avons toujours un tableau à parcourir
1609
+ if (!Array.isArray(resolvedRecipients)) {
1610
+ resolvedRecipients = [resolvedRecipients];
1611
+ }
1502
1612
 
1503
- const rto = (await Promise.all(emailRecipients.map(r => substituteVariables(r, context, user))));
1504
- const rsubject = await substituteVariables(emailSubject, context, user);
1505
- const rbody = await substituteVariables(emailContent, context, user);
1613
+ resolvedRecipients = resolvedRecipients.flat();
1506
1614
 
1507
- // 3. Préparer les données pour sendEmail
1508
- const emailData = {
1509
- title: rsubject,
1510
- content: rbody
1511
- };
1615
+ if (resolvedRecipients.length === 0) {
1616
+ return { success: true, message: "No recipients found after substitution. Nothing to send." };
1617
+ }
1618
+
1619
+ logger.info(`[handleSendEmailAction] Preparing to send emails to ${resolvedRecipients.length} recipient(s).`);
1620
+
1621
+ const allPromises = [];
1622
+ const sentTo = [];
1623
+ const failedFor = [];
1512
1624
 
1513
- await sendEmail(rto, emailData, smtpConfig, user.lang);
1514
- logger.info(`[Workflow] Action sendEmail terminée avec succès pour le destinataire: ${rto}`);
1625
+ // 4. Itérer sur chaque destinataire pour envoyer un e-mail personnalisé
1626
+ for (const recipient of resolvedRecipients) {
1627
+ // Le destinataire peut être une simple chaîne (email) ou un objet { email: '...', nom: '...' }
1628
+ const recipientEmail = typeof recipient === 'object' && recipient !== null ? recipient.email : recipient;
1515
1629
 
1516
- return { success: true, message: `Email sent to ${rto.join(', ')}` };
1630
+ if (!recipientEmail || typeof recipientEmail !== 'string') {
1631
+ logger.warn(`[handleSendEmailAction] Skipping an invalid recipient entry:`, recipient);
1632
+ failedFor.push(recipient); // Garder une trace de l'entrée invalide
1633
+ continue;
1634
+ }
1635
+
1636
+
1637
+ // 5. Créer un contexte personnalisé pour ce destinataire spécifique
1638
+ // Cela permet d'utiliser des placeholders comme {recipient.name}
1639
+ const personalizedContext = { ...contextData, recipient };
1640
+
1641
+ // 6. Substituer les variables dans le sujet et le contenu pour ce destinataire
1642
+ const personalizedSubject = await substituteVariables(emailSubject, personalizedContext, user);
1643
+ const personalizedBody = await substituteVariables(emailContent, personalizedContext, user);
1644
+
1645
+ const emailData = { title: personalizedSubject, content: personalizedBody };
1646
+
1647
+ // 7. Envoyer l'e-mail et suivre son résultat
1648
+ const sendPromise = sendEmail([recipientEmail], emailData, smtpConfig, user.lang)
1649
+ .then(() => {
1650
+ sentTo.push(recipient);
1651
+ })
1652
+ .catch(err => {
1653
+ logger.error(`[handleSendEmailAction] Failed to send email to ${recipientEmail}: ${err.message}`);
1654
+ failedFor.push({ recipient: recipientEmail, error: err.message });
1655
+ });
1656
+
1657
+ allPromises.push(sendPromise);
1658
+ }
1659
+
1660
+ // Attendre que toutes les tentatives d'envoi soient terminées
1661
+ await Promise.all(allPromises);
1662
+
1663
+ const summaryMessage = `Email process completed. Sent: ${sentTo.length}. Failed: ${failedFor.length}.`;
1664
+ logger.info(`[handleSendEmailAction] ${summaryMessage}`);
1665
+
1666
+ // L'action elle-même a réussi, même si certains e-mails ont échoué.
1667
+ // Le message de retour et les données fournissent les détails.
1668
+ return {
1669
+ success: true,
1670
+ message: summaryMessage,
1671
+ data: {
1672
+ sent: sentTo,
1673
+ failed: failedFor
1674
+ },
1675
+ updatedContext: {
1676
+ emailResult: {
1677
+ sent: sentTo,
1678
+ failed: failedFor
1679
+ }
1680
+ }
1681
+ };
1517
1682
 
1518
1683
  } catch (error) {
1519
- logger.error(`[handleSendEmailAction] Erreur lors de l'envoi de l'email : ${error.message}`, error.stack);
1520
- return { success: false, message: error.message };
1684
+ const msg = `[handleSendEmailAction] Unexpected error during email processing: ${error.message}`;
1685
+ logger.error(msg, error.stack);
1686
+ return { success: false, message: msg };
1521
1687
  }
1522
1688
  }