@happyvertical/smrt-messages 0.60.8 → 0.60.10

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.
Files changed (38) hide show
  1. package/AGENTS.md +17 -0
  2. package/README.md +40 -0
  3. package/agents/notification-outbox.md +116 -0
  4. package/dist/chunks/{AccountCollection-O-zbituV.js → AccountCollection-By48bdwt.js} +2 -2
  5. package/dist/chunks/{AccountCollection-O-zbituV.js.map → AccountCollection-By48bdwt.js.map} +1 -1
  6. package/dist/chunks/{Email-MRtj6D2s.js → Email-CMUK3aWU.js} +3 -3
  7. package/dist/chunks/{Email-MRtj6D2s.js.map → Email-CMUK3aWU.js.map} +1 -1
  8. package/dist/chunks/{EmailFolder-qpOwfo_O.js → EmailFolder-DLJvqNqI.js} +2 -2
  9. package/dist/chunks/{EmailFolder-qpOwfo_O.js.map → EmailFolder-DLJvqNqI.js.map} +1 -1
  10. package/dist/chunks/{Message-5Xz9tauF.js → Message-DXpqot5f.js} +52 -5
  11. package/dist/chunks/Message-DXpqot5f.js.map +1 -0
  12. package/dist/chunks/{MessageCollection-m1C6fDwS.js → MessageCollection-CMGaSGBc.js} +2 -2
  13. package/dist/chunks/{MessageCollection-m1C6fDwS.js.map → MessageCollection-CMGaSGBc.js.map} +1 -1
  14. package/dist/chunks/__smrt-register__-CaGLtiEU.js +6 -0
  15. package/dist/chunks/__smrt-register__-CaGLtiEU.js.map +1 -0
  16. package/dist/collections/NotificationDeliveryCollection.d.ts +6 -0
  17. package/dist/collections/NotificationDeliveryCollection.d.ts.map +1 -0
  18. package/dist/index.d.ts +3 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +780 -38
  21. package/dist/index.js.map +1 -1
  22. package/dist/manifest.json +1135 -89
  23. package/dist/models/Message.d.ts +15 -0
  24. package/dist/models/Message.d.ts.map +1 -1
  25. package/dist/models/NotificationDelivery.d.ts +128 -0
  26. package/dist/models/NotificationDelivery.d.ts.map +1 -0
  27. package/dist/senders/EmailSender.d.ts +9 -0
  28. package/dist/senders/EmailSender.d.ts.map +1 -1
  29. package/dist/services/NotificationOutboxService.d.ts +226 -0
  30. package/dist/services/NotificationOutboxService.d.ts.map +1 -0
  31. package/dist/smrt-knowledge.json +470 -9
  32. package/dist/smrt-knowledge.okf.md +155 -0
  33. package/dist/types.d.ts +6 -0
  34. package/dist/types.d.ts.map +1 -1
  35. package/package.json +17 -16
  36. package/dist/chunks/Message-5Xz9tauF.js.map +0 -1
  37. package/dist/chunks/__smrt-register__-DwNoP4GB.js +0 -6
  38. package/dist/chunks/__smrt-register__-DwNoP4GB.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,16 +1,16 @@
1
1
  import { t as __exportAll } from "./chunks/rolldown-runtime-D7D4PA-g.js";
2
- import "./chunks/__smrt-register__-DwNoP4GB.js";
2
+ import "./chunks/__smrt-register__-CaGLtiEU.js";
3
3
  import { a as registerMessagingProvider, n as getMessagingProvider, o as ZulipSender, r as listMessagingProviders, s as TelegramSender, t as ensureBuiltinMessagingProvidersRegistered } from "./chunks/providers-4iLula-V.js";
4
- import { r as Account, t as AccountCollection } from "./chunks/AccountCollection-O-zbituV.js";
5
- import { t as Message } from "./chunks/Message-5Xz9tauF.js";
6
- import { t as MessageCollection } from "./chunks/MessageCollection-m1C6fDwS.js";
7
- import { t as Email } from "./chunks/Email-MRtj6D2s.js";
8
- import { t as EmailFolder } from "./chunks/EmailFolder-qpOwfo_O.js";
4
+ import { r as Account, t as AccountCollection } from "./chunks/AccountCollection-By48bdwt.js";
5
+ import { t as Message } from "./chunks/Message-DXpqot5f.js";
6
+ import { t as MessageCollection } from "./chunks/MessageCollection-CMGaSGBc.js";
7
+ import { t as Email } from "./chunks/Email-CMUK3aWU.js";
8
+ import { t as EmailFolder } from "./chunks/EmailFolder-DLJvqNqI.js";
9
9
  import { t as SlackSender } from "./chunks/SlackSender-CjSvwY1K.js";
10
10
  import { t as TweetSender } from "./chunks/TweetSender-CcLPNNi1.js";
11
- import { SmrtCollection, SmrtObject, SmrtRecipe, crossPackageRef, field, foreignKey, importOptionalDependency, isUniqueViolationError, smrt } from "@happyvertical/smrt-core";
11
+ import { SmrtCollection, SmrtObject, SmrtRecipe, crossPackageRef, field, foreignKey, importOptionalDependency, isUniqueViolationError, smrt, usesEmbeddedRevisionFallback, withEmbeddedWriteQueue } from "@happyvertical/smrt-core";
12
12
  import { registerPermissionDefinitions } from "@happyvertical/smrt-users";
13
- import { TenantScoped, queryGlobal, queryWithGlobals, tenantId, withTenant } from "@happyvertical/smrt-tenancy";
13
+ import { TenantScoped, queryGlobal, queryWithGlobals, tenantId, withSystemContext, withTenant } from "@happyvertical/smrt-tenancy";
14
14
  //#region src/permissions.ts
15
15
  var SEND_MESSAGES_PERMISSION = "messages.send";
16
16
  var MANAGE_MESSAGE_ROUTES_PERMISSION = "messages.manage-routes";
@@ -54,21 +54,21 @@ function ensureMessagingPermissionsRegistered() {
54
54
  }
55
55
  //#endregion
56
56
  //#region src/models/Attachment.ts
57
- var __defProp$12 = Object.defineProperty;
58
- var __getOwnPropDesc$12 = Object.getOwnPropertyDescriptor;
59
- var __defNormalProp$12 = (obj, key, value) => key in obj ? __defProp$12(obj, key, {
57
+ var __defProp$13 = Object.defineProperty;
58
+ var __getOwnPropDesc$13 = Object.getOwnPropertyDescriptor;
59
+ var __defNormalProp$13 = (obj, key, value) => key in obj ? __defProp$13(obj, key, {
60
60
  enumerable: true,
61
61
  configurable: true,
62
62
  writable: true,
63
63
  value
64
64
  }) : obj[key] = value;
65
- var __decorateClass$12 = (decorators, target, key, kind) => {
66
- var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$12(target, key) : target;
65
+ var __decorateClass$13 = (decorators, target, key, kind) => {
66
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$13(target, key) : target;
67
67
  for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
68
- if (kind && result) __defProp$12(target, key, result);
68
+ if (kind && result) __defProp$13(target, key, result);
69
69
  return result;
70
70
  };
71
- var __publicField$12 = (obj, key, value) => __defNormalProp$12(obj, typeof key !== "symbol" ? key + "" : key, value);
71
+ var __publicField$13 = (obj, key, value) => __defNormalProp$13(obj, typeof key !== "symbol" ? key + "" : key, value);
72
72
  function splitAbsolutePath(filePath) {
73
73
  const windows = /^[A-Za-z]:[\\/]/.test(filePath);
74
74
  if (!windows && !filePath.startsWith("/")) return null;
@@ -107,7 +107,7 @@ var Attachment = class extends SmrtObject {
107
107
  */
108
108
  async getMessage() {
109
109
  if (!this.messageId) return null;
110
- const { MessageCollection } = await import("./chunks/MessageCollection-m1C6fDwS.js").then((n) => n.n);
110
+ const { MessageCollection } = await import("./chunks/MessageCollection-CMGaSGBc.js").then((n) => n.n);
111
111
  return await (await MessageCollection.create(this.options)).get({ id: this.messageId });
112
112
  }
113
113
  /**
@@ -187,10 +187,10 @@ var Attachment = class extends SmrtObject {
187
187
  }
188
188
  }
189
189
  };
190
- __publicField$12(Attachment, "__smrtPackage__", "@happyvertical/smrt-messages");
191
- __decorateClass$12([tenantId({ nullable: true })], Attachment.prototype, "tenantId", 2);
192
- __decorateClass$12([foreignKey("Message")], Attachment.prototype, "messageId", 2);
193
- Attachment = __decorateClass$12([TenantScoped({ mode: "optional" }), smrt({
190
+ __publicField$13(Attachment, "__smrtPackage__", "@happyvertical/smrt-messages");
191
+ __decorateClass$13([tenantId({ nullable: true })], Attachment.prototype, "tenantId", 2);
192
+ __decorateClass$13([foreignKey("Message")], Attachment.prototype, "messageId", 2);
193
+ Attachment = __decorateClass$13([TenantScoped({ mode: "optional" }), smrt({
194
194
  api: { include: [
195
195
  "list",
196
196
  "get",
@@ -317,21 +317,21 @@ var AttachmentCollection = class extends SmrtCollection {
317
317
  };
318
318
  //#endregion
319
319
  //#region src/models/MessagingEndpoint.ts
320
- var __defProp$11 = Object.defineProperty;
321
- var __getOwnPropDesc$11 = Object.getOwnPropertyDescriptor;
322
- var __defNormalProp$11 = (obj, key, value) => key in obj ? __defProp$11(obj, key, {
320
+ var __defProp$12 = Object.defineProperty;
321
+ var __getOwnPropDesc$12 = Object.getOwnPropertyDescriptor;
322
+ var __defNormalProp$12 = (obj, key, value) => key in obj ? __defProp$12(obj, key, {
323
323
  enumerable: true,
324
324
  configurable: true,
325
325
  writable: true,
326
326
  value
327
327
  }) : obj[key] = value;
328
- var __decorateClass$11 = (decorators, target, key, kind) => {
329
- var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$11(target, key) : target;
328
+ var __decorateClass$12 = (decorators, target, key, kind) => {
329
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$12(target, key) : target;
330
330
  for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
331
- if (kind && result) __defProp$11(target, key, result);
331
+ if (kind && result) __defProp$12(target, key, result);
332
332
  return result;
333
333
  };
334
- var __publicField$11 = (obj, key, value) => __defNormalProp$11(obj, typeof key !== "symbol" ? key + "" : key, value);
334
+ var __publicField$12 = (obj, key, value) => __defNormalProp$12(obj, typeof key !== "symbol" ? key + "" : key, value);
335
335
  var MessagingEndpoint = class extends SmrtObject {
336
336
  tenantId = "";
337
337
  profileId = null;
@@ -376,16 +376,16 @@ var MessagingEndpoint = class extends SmrtObject {
376
376
  return value.length <= 4 ? "••••" : `\u2022\u2022\u2022\u2022${value.slice(-4)}`;
377
377
  }
378
378
  };
379
- __publicField$11(MessagingEndpoint, "__smrtPackage__", "@happyvertical/smrt-messages");
380
- __decorateClass$11([tenantId()], MessagingEndpoint.prototype, "tenantId", 2);
381
- __decorateClass$11([crossPackageRef("@happyvertical/smrt-profiles:Profile", { nullable: true })], MessagingEndpoint.prototype, "profileId", 2);
382
- __decorateClass$11([field({ required: true })], MessagingEndpoint.prototype, "label", 2);
383
- __decorateClass$11([field({ required: true })], MessagingEndpoint.prototype, "channel", 2);
384
- __decorateClass$11([field({
379
+ __publicField$12(MessagingEndpoint, "__smrtPackage__", "@happyvertical/smrt-messages");
380
+ __decorateClass$12([tenantId()], MessagingEndpoint.prototype, "tenantId", 2);
381
+ __decorateClass$12([crossPackageRef("@happyvertical/smrt-profiles:Profile", { nullable: true })], MessagingEndpoint.prototype, "profileId", 2);
382
+ __decorateClass$12([field({ required: true })], MessagingEndpoint.prototype, "label", 2);
383
+ __decorateClass$12([field({ required: true })], MessagingEndpoint.prototype, "channel", 2);
384
+ __decorateClass$12([field({
385
385
  required: true,
386
386
  sensitive: true
387
387
  })], MessagingEndpoint.prototype, "address", 2);
388
- MessagingEndpoint = __decorateClass$11([TenantScoped({ mode: "required" }), smrt({
388
+ MessagingEndpoint = __decorateClass$12([TenantScoped({ mode: "required" }), smrt({
389
389
  tableName: "messaging_endpoints",
390
390
  api: { include: ["list", "get"] },
391
391
  mcp: { include: ["list", "get"] },
@@ -403,6 +403,196 @@ var MessagingEndpointCollection = class extends SmrtCollection {
403
403
  static _itemClass = MessagingEndpoint;
404
404
  };
405
405
  //#endregion
406
+ //#region src/models/NotificationDelivery.ts
407
+ var __defProp$11 = Object.defineProperty;
408
+ var __getOwnPropDesc$11 = Object.getOwnPropertyDescriptor;
409
+ var __defNormalProp$11 = (obj, key, value) => key in obj ? __defProp$11(obj, key, {
410
+ enumerable: true,
411
+ configurable: true,
412
+ writable: true,
413
+ value
414
+ }) : obj[key] = value;
415
+ var __decorateClass$11 = (decorators, target, key, kind) => {
416
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$11(target, key) : target;
417
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
418
+ if (kind && result) __defProp$11(target, key, result);
419
+ return result;
420
+ };
421
+ var __publicField$11 = (obj, key, value) => __defNormalProp$11(obj, typeof key !== "symbol" ? key + "" : key, value);
422
+ var NOTIFICATION_DELIVERY_CHANNELS = ["email"];
423
+ var TRANSITION_COLUMNS = {
424
+ messageId: "message_id",
425
+ status: "status",
426
+ attempts: "attempts",
427
+ nextAttemptAt: "next_attempt_at",
428
+ leaseToken: "lease_token",
429
+ leaseExpiresAt: "lease_expires_at",
430
+ handedOffAt: "handed_off_at",
431
+ deliveredAt: "delivered_at",
432
+ providerMessageId: "provider_message_id",
433
+ lastError: "last_error"
434
+ };
435
+ var NotificationDelivery = class extends SmrtObject {
436
+ tenantId = "";
437
+ recipientUserId = "";
438
+ channel = "email";
439
+ sourceRef = "";
440
+ kind = "";
441
+ subject = "";
442
+ textBody = "";
443
+ href = "";
444
+ recipientAddress = "";
445
+ /** Display name snapshot from the host recipient resolver. */
446
+ recipientName = "";
447
+ accountId = "";
448
+ messageId = null;
449
+ status = "pending";
450
+ /** Compare-and-set counter; bumped by every transition. */
451
+ stateVersion = 0;
452
+ /** Unique id of the transition that wrote the current state. */
453
+ transitionId = "";
454
+ /** Leases acquired so far (provider handoffs are a subset). */
455
+ attempts = 0;
456
+ /** Lease budget before the delivery fails for good. */
457
+ maxAttempts = 5;
458
+ /** Earliest time a dispatcher may lease a `pending` row. */
459
+ nextAttemptAt = /* @__PURE__ */ new Date();
460
+ /** Proof of the current lease holder; cleared when the lease ends. */
461
+ leaseToken = null;
462
+ /** When the current lease lapses and the row becomes recoverable. */
463
+ leaseExpiresAt = null;
464
+ /** Set just before the current attempt reaches the provider. */
465
+ handedOffAt = null;
466
+ /** When the provider accepted the message. */
467
+ deliveredAt = null;
468
+ /** Provider message id, when the provider returns one. */
469
+ providerMessageId = "";
470
+ /** The latest failure or uncertainty reason. */
471
+ lastError = "";
472
+ constructor(options = {}) {
473
+ super(options);
474
+ const assign = (key) => {
475
+ if (options[key] !== void 0) this[key] = options[key];
476
+ };
477
+ for (const key of [
478
+ "tenantId",
479
+ "recipientUserId",
480
+ "channel",
481
+ "sourceRef",
482
+ "kind",
483
+ "subject",
484
+ "textBody",
485
+ "href",
486
+ "recipientAddress",
487
+ "recipientName",
488
+ "accountId",
489
+ "messageId",
490
+ "status",
491
+ "stateVersion",
492
+ "transitionId",
493
+ "attempts",
494
+ "maxAttempts",
495
+ "nextAttemptAt",
496
+ "leaseToken",
497
+ "leaseExpiresAt",
498
+ "handedOffAt",
499
+ "deliveredAt",
500
+ "providerMessageId",
501
+ "lastError"
502
+ ]) assign(key);
503
+ }
504
+ /** Whether no further automatic work will happen on this row. */
505
+ get isSettled() {
506
+ return this.status === "sent" || this.status === "failed" || this.status === "cancelled" || this.status === "uncertain";
507
+ }
508
+ /**
509
+ * Apply `changes` only if the stored row is still at `stateVersion` (and,
510
+ * when `leaseToken` is given, still held by that lease). Returns whether
511
+ * this caller won; on success the instance reflects the stored row.
512
+ *
513
+ * PostgreSQL and remote LibSQL evaluate the guard in one conditional
514
+ * UPDATE; local embedded engines use the process write queue, matching
515
+ * `Message.send()`'s claim. The durable row is re-read either way, and the
516
+ * write is recognised by its unique `transitionId`, because DuckDB's update
517
+ * result over-reports affected rows.
518
+ */
519
+ async transition(changes, expected = {}) {
520
+ if (!this.id) throw new Error("Cannot transition an unsaved delivery.");
521
+ const fromVersion = this.stateVersion;
522
+ const nextVersion = fromVersion + 1;
523
+ const transitionId = crypto.randomUUID();
524
+ const values = {
525
+ state_version: nextVersion,
526
+ transition_id: transitionId,
527
+ updated_at: /* @__PURE__ */ new Date()
528
+ };
529
+ for (const [key, value] of Object.entries(changes)) {
530
+ const column = TRANSITION_COLUMNS[key];
531
+ if (!column) throw new Error(`'${key}' is not a transition field.`);
532
+ values[column] = value;
533
+ }
534
+ const matches = (row) => row !== null && Number(row.state_version) === fromVersion && (expected.leaseToken === void 0 || row.lease_token === expected.leaseToken);
535
+ const useEmbeddedFallback = usesEmbeddedRevisionFallback(this.db);
536
+ await withEmbeddedWriteQueue(this.db, useEmbeddedFallback, async () => {
537
+ if (useEmbeddedFallback) {
538
+ const current = await this.getCanonicalPersistedRow({ id: this.id });
539
+ if (!matches(current)) return;
540
+ await this.db.upsert(this.tableName, ["id"], {
541
+ ...current,
542
+ ...values
543
+ });
544
+ return;
545
+ }
546
+ await this.db.update(this.tableName, {
547
+ id: this.id,
548
+ tenant_id: this.tenantId,
549
+ state_version: fromVersion,
550
+ ...expected.leaseToken === void 0 ? {} : { lease_token: expected.leaseToken }
551
+ }, values);
552
+ });
553
+ const verified = await this.getCanonicalPersistedRow({ id: this.id });
554
+ if (!verified || verified.transition_id !== transitionId) return false;
555
+ await this.completePersistedUpdate(verified);
556
+ return true;
557
+ }
558
+ };
559
+ __publicField$11(NotificationDelivery, "__smrtPackage__", "@happyvertical/smrt-messages");
560
+ __decorateClass$11([tenantId()], NotificationDelivery.prototype, "tenantId", 2);
561
+ __decorateClass$11([crossPackageRef("@happyvertical/smrt-users:User", { required: true })], NotificationDelivery.prototype, "recipientUserId", 2);
562
+ __decorateClass$11([field({ required: true })], NotificationDelivery.prototype, "channel", 2);
563
+ __decorateClass$11([field({ required: true })], NotificationDelivery.prototype, "sourceRef", 2);
564
+ __decorateClass$11([field({ required: true })], NotificationDelivery.prototype, "kind", 2);
565
+ __decorateClass$11([field({ required: true })], NotificationDelivery.prototype, "subject", 2);
566
+ __decorateClass$11([field({ required: true })], NotificationDelivery.prototype, "textBody", 2);
567
+ __decorateClass$11([field({ required: true })], NotificationDelivery.prototype, "href", 2);
568
+ __decorateClass$11([field({ required: true })], NotificationDelivery.prototype, "recipientAddress", 2);
569
+ __decorateClass$11([foreignKey("Account")], NotificationDelivery.prototype, "accountId", 2);
570
+ __decorateClass$11([foreignKey("Message")], NotificationDelivery.prototype, "messageId", 2);
571
+ NotificationDelivery = __decorateClass$11([TenantScoped({ mode: "required" }), smrt({
572
+ tableName: "notification_deliveries",
573
+ api: false,
574
+ mcp: false,
575
+ cli: false,
576
+ conflictColumns: [
577
+ "tenant_id",
578
+ "recipient_user_id",
579
+ "channel",
580
+ "source_ref"
581
+ ],
582
+ indexes: [{
583
+ name: "notification_deliveries_due_idx",
584
+ columns: ["status", "nextAttemptAt"]
585
+ }, {
586
+ name: "notification_deliveries_lease_idx",
587
+ columns: ["status", "leaseExpiresAt"]
588
+ }]
589
+ })], NotificationDelivery);
590
+ //#endregion
591
+ //#region src/collections/NotificationDeliveryCollection.ts
592
+ var NotificationDeliveryCollection = class extends SmrtCollection {
593
+ static _itemClass = NotificationDelivery;
594
+ };
595
+ //#endregion
406
596
  //#region src/models/PersonaMessageRoute.ts
407
597
  var __defProp$10 = Object.defineProperty;
408
598
  var __getOwnPropDesc$10 = Object.getOwnPropertyDescriptor;
@@ -688,7 +878,7 @@ var EmailAccount = class extends Account {
688
878
  for (const folderName of foldersToSync) try {
689
879
  let folder = await folderCollection.getByPath(accountId, folderName);
690
880
  if (!folder) {
691
- const { EmailFolder } = await import("./chunks/EmailFolder-qpOwfo_O.js").then((n) => n.n);
881
+ const { EmailFolder } = await import("./chunks/EmailFolder-DLJvqNqI.js").then((n) => n.n);
692
882
  folder = new EmailFolder({
693
883
  ...this.childOptions(),
694
884
  accountId,
@@ -713,7 +903,7 @@ var EmailAccount = class extends Account {
713
903
  result.messagesSkipped++;
714
904
  continue;
715
905
  }
716
- const { Email } = await import("./chunks/Email-MRtj6D2s.js").then((n) => n.n);
906
+ const { Email } = await import("./chunks/Email-CMUK3aWU.js").then((n) => n.n);
717
907
  let email = existingEmail;
718
908
  if (!email) {
719
909
  email = new Email({
@@ -1924,7 +2114,10 @@ ZulipAccount = __decorateClass([smrt({
1924
2114
  })], ZulipAccount);
1925
2115
  //#endregion
1926
2116
  //#region src/senders/EmailSender.ts
1927
- var EmailSender_exports = /* @__PURE__ */ __exportAll({ EmailSender: () => EmailSender });
2117
+ var EmailSender_exports = /* @__PURE__ */ __exportAll({
2118
+ EmailSender: () => EmailSender,
2119
+ isDefiniteSendRejection: () => isDefiniteSendRejection
2120
+ });
1928
2121
  var EmailSender = class {
1929
2122
  providerType = "email";
1930
2123
  client;
@@ -1987,6 +2180,7 @@ var EmailSender = class {
1987
2180
  return {
1988
2181
  success: false,
1989
2182
  error: error instanceof Error ? error.message : String(error),
2183
+ ...isDefiniteSendRejection(error) ? {} : { outcomeUnknown: true },
1990
2184
  sentAt: /* @__PURE__ */ new Date()
1991
2185
  };
1992
2186
  }
@@ -2033,6 +2227,31 @@ var EmailSender = class {
2033
2227
  return resolved;
2034
2228
  }
2035
2229
  };
2230
+ var DEFINITE_EMAIL_ERROR_CODES = /* @__PURE__ */ new Set([
2231
+ "AUTHENTICATION_ERROR",
2232
+ "AUTHORIZATION_ERROR",
2233
+ "INVALID_MESSAGE",
2234
+ "ATTACHMENT_ERROR"
2235
+ ]);
2236
+ var NEVER_CONNECTED_CODES = /* @__PURE__ */ new Set([
2237
+ "ECONNREFUSED",
2238
+ "ENOTFOUND",
2239
+ "EHOSTUNREACH",
2240
+ "EAI_AGAIN"
2241
+ ]);
2242
+ function isDefiniteSendRejection(error) {
2243
+ if (!error || typeof error !== "object") return false;
2244
+ const candidates = [error, error.cause].filter((value) => !!value && typeof value === "object");
2245
+ for (const candidate of candidates) {
2246
+ if (typeof candidate.responseCode === "number") return true;
2247
+ if (typeof candidate.code === "string") {
2248
+ if (DEFINITE_EMAIL_ERROR_CODES.has(candidate.code)) return true;
2249
+ if (NEVER_CONNECTED_CODES.has(candidate.code)) return true;
2250
+ }
2251
+ }
2252
+ const sendError = error;
2253
+ return sendError.code === "SEND_ERROR" && Array.isArray(sendError.accepted) && sendError.accepted.length === 0;
2254
+ }
2036
2255
  //#endregion
2037
2256
  //#region src/services/MessagingSettingsService.ts
2038
2257
  var MessagingSettingsService = class {
@@ -2231,6 +2450,529 @@ var MessagingSettingsService = class {
2231
2450
  }
2232
2451
  };
2233
2452
  //#endregion
2453
+ //#region src/services/NotificationOutboxService.ts
2454
+ var messageSendTransport = {
2455
+ name: "message-send",
2456
+ idempotent: false,
2457
+ async send({ message }) {
2458
+ const result = await message.send();
2459
+ if (result.success) return {
2460
+ status: "accepted",
2461
+ ...result.providerMessageId ? { providerMessageId: result.providerMessageId } : {}
2462
+ };
2463
+ if (result.outcomeUnknown || message.sendStatus === "sending" || message.sendStatus === "sent") return {
2464
+ status: "unknown",
2465
+ error: result.error ?? "The provider did not confirm the outcome."
2466
+ };
2467
+ return {
2468
+ status: "rejected",
2469
+ error: result.error ?? "Send failed",
2470
+ retryable: true
2471
+ };
2472
+ },
2473
+ async reconcile({ message }) {
2474
+ const messages = await MessageCollection.create({ db: message.db });
2475
+ const stored = message.id ? await messages.get({ id: message.id }) : null;
2476
+ switch (stored?.sendStatus) {
2477
+ case "sent": return { status: "accepted" };
2478
+ case "failed":
2479
+ if (stored.isSendOutcomeUnknown()) return {
2480
+ status: "unknown",
2481
+ error: stored.sendError || "The provider did not confirm the outcome."
2482
+ };
2483
+ return {
2484
+ status: "rejected",
2485
+ error: stored.sendError || "Send failed",
2486
+ retryable: true
2487
+ };
2488
+ case "draft":
2489
+ case "pending":
2490
+ case "scheduled": return { status: "not-sent" };
2491
+ default: return {
2492
+ status: "unknown",
2493
+ error: "The provider handoff did not record an outcome."
2494
+ };
2495
+ }
2496
+ }
2497
+ };
2498
+ var NotificationOutboxDeniedError = class extends Error {
2499
+ constructor(reason, message) {
2500
+ super(message);
2501
+ this.reason = reason;
2502
+ this.name = "NotificationOutboxDeniedError";
2503
+ }
2504
+ reason;
2505
+ };
2506
+ var UUID_PATTERN$1 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2507
+ var DEFAULT_LEASE_MS = 5 * 6e4;
2508
+ var DEFAULT_MAX_ATTEMPTS = 5;
2509
+ var DEFAULT_LIMIT$1 = 25;
2510
+ var MAX_LIMIT$1 = 200;
2511
+ function defaultRetryDelayMs(attempts) {
2512
+ return Math.min(6e4 * 2 ** Math.max(0, attempts - 1), 60 * 6e4);
2513
+ }
2514
+ function requireText$1(value, name) {
2515
+ const text = value?.trim();
2516
+ if (!text) throw new Error(`A notification ${name} is required.`);
2517
+ return text;
2518
+ }
2519
+ function errorText(error) {
2520
+ return error instanceof Error ? error.message : String(error);
2521
+ }
2522
+ async function notificationDeliveryMessageId(deliveryId) {
2523
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`smrt-messages:notification-delivery:${deliveryId}`));
2524
+ const bytes = new Uint8Array(digest).slice(0, 16);
2525
+ bytes[6] = (bytes[6] ?? 0) & 15 | 80;
2526
+ bytes[8] = (bytes[8] ?? 0) & 63 | 128;
2527
+ const hex = [...bytes].map((byte) => byte.toString(16).padStart(2, "0"));
2528
+ return [
2529
+ hex.slice(0, 4),
2530
+ hex.slice(4, 6),
2531
+ hex.slice(6, 8),
2532
+ hex.slice(8, 10),
2533
+ hex.slice(10, 16)
2534
+ ].map((group) => group.join("")).join("-");
2535
+ }
2536
+ function notificationDeliveryCorrelationId(deliveryId) {
2537
+ return `notification-delivery:${deliveryId}`;
2538
+ }
2539
+ function buildNotificationLink(baseUrl, path) {
2540
+ let base;
2541
+ try {
2542
+ base = new URL(baseUrl);
2543
+ } catch {
2544
+ throw new Error("Notification link base URL must be an absolute URL.");
2545
+ }
2546
+ if (base.protocol !== "https:" && base.protocol !== "http:") throw new Error("Notification link base URL must use http or https.");
2547
+ if (base.username || base.password || base.search || base.hash) throw new Error("Notification link base URL must not carry credentials, a query, or a fragment.");
2548
+ if (typeof path !== "string" || !path.startsWith("/") || path.startsWith("//") || /[\\\s]/u.test(path) || /[\u0000-\u001f\u007f]/u.test(path)) throw new NotificationOutboxDeniedError("link", "Notification links must be a host-relative path such as /inbox/123.");
2549
+ const prefix = base.pathname.replace(/\/+$/u, "");
2550
+ const link = new URL(`${prefix}${path}`, base.origin);
2551
+ if (link.origin !== base.origin) throw new NotificationOutboxDeniedError("link", "Notification links must stay on the configured origin.");
2552
+ return link.toString();
2553
+ }
2554
+ var NotificationOutboxService = class {
2555
+ constructor(options) {
2556
+ this.options = options;
2557
+ buildNotificationLink(options.linkBaseUrl, "/");
2558
+ this.transport = options.transport ?? messageSendTransport;
2559
+ this.leaseMs = options.leaseMs ?? DEFAULT_LEASE_MS;
2560
+ this.maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
2561
+ this.retryDelayMs = options.retryDelayMs ?? defaultRetryDelayMs;
2562
+ this.now = options.now ?? (() => /* @__PURE__ */ new Date());
2563
+ if (!Number.isInteger(this.leaseMs) || this.leaseMs < 1e3) throw new Error("Notification lease must be at least 1000 ms.");
2564
+ if (!Number.isInteger(this.maxAttempts) || this.maxAttempts < 1) throw new Error("Notification maxAttempts must be a positive integer.");
2565
+ }
2566
+ options;
2567
+ transport;
2568
+ leaseMs;
2569
+ maxAttempts;
2570
+ retryDelayMs;
2571
+ now;
2572
+ collection() {
2573
+ return NotificationDeliveryCollection.create({ db: this.options.db });
2574
+ }
2575
+ authorize(tenantId) {
2576
+ if (!this.options.principal) return;
2577
+ try {
2578
+ assertMessagingPermission(this.options.principal, tenantId, SEND_MESSAGES_PERMISSION);
2579
+ } catch (error) {
2580
+ throw new NotificationOutboxDeniedError("permission", errorText(error));
2581
+ }
2582
+ }
2583
+ /** Store one email delivery for the event unless `sourceRef` already has one. */
2584
+ async enqueueEmail(input) {
2585
+ const tenantId = requireText$1(input.tenantId, "tenant");
2586
+ const recipientUserId = requireText$1(input.recipientUserId, "recipient");
2587
+ const sourceRef = requireText$1(input.sourceRef, "sourceRef");
2588
+ const kind = requireText$1(input.kind, "kind");
2589
+ const subject = requireText$1(input.subject, "subject");
2590
+ const text = requireText$1(input.text, "text");
2591
+ if (/[\r\n]/u.test(subject)) throw new Error("A notification subject must be a single line.");
2592
+ const channel = "email";
2593
+ this.authorize(tenantId);
2594
+ const href = buildNotificationLink(this.options.linkBaseUrl, input.path);
2595
+ return withTenant({ tenantId }, async () => {
2596
+ const deliveries = await this.collection();
2597
+ const findExisting = async () => (await deliveries.list({
2598
+ where: {
2599
+ tenantId,
2600
+ recipientUserId,
2601
+ channel,
2602
+ sourceRef
2603
+ },
2604
+ limit: 1
2605
+ }))[0];
2606
+ const existing = await findExisting();
2607
+ if (existing) return {
2608
+ delivery: existing,
2609
+ created: false
2610
+ };
2611
+ const recipient = await this.options.resolveRecipient({
2612
+ tenantId,
2613
+ recipientUserId,
2614
+ channel
2615
+ });
2616
+ const address = recipient?.address?.trim();
2617
+ if (!address || /[\s,;<>]/u.test(address) || !address.includes("@")) throw new NotificationOutboxDeniedError("recipient", "The host did not authorize an email recipient for this user.");
2618
+ const accountId = await this.requireSenderAccount(tenantId);
2619
+ const suppressed = await this.isSuppressed({
2620
+ tenantId,
2621
+ recipientUserId,
2622
+ channel,
2623
+ kind,
2624
+ sourceRef
2625
+ });
2626
+ try {
2627
+ return {
2628
+ delivery: await deliveries.create({
2629
+ _insertOnly: true,
2630
+ tenantId,
2631
+ recipientUserId,
2632
+ channel,
2633
+ sourceRef,
2634
+ kind,
2635
+ subject,
2636
+ textBody: text,
2637
+ href,
2638
+ recipientAddress: address,
2639
+ recipientName: recipient?.name?.trim() ?? "",
2640
+ accountId,
2641
+ messageId: null,
2642
+ status: suppressed ? "cancelled" : "pending",
2643
+ stateVersion: 0,
2644
+ transitionId: "",
2645
+ attempts: 0,
2646
+ maxAttempts: this.maxAttempts,
2647
+ nextAttemptAt: input.notBefore ?? this.now(),
2648
+ leaseToken: null,
2649
+ leaseExpiresAt: null,
2650
+ handedOffAt: null,
2651
+ deliveredAt: null,
2652
+ providerMessageId: "",
2653
+ lastError: suppressed ? "Suppressed by recipient preference." : ""
2654
+ }),
2655
+ created: true
2656
+ };
2657
+ } catch (error) {
2658
+ if (!isUniqueViolationError(error)) throw error;
2659
+ const winner = await findExisting();
2660
+ if (!winner) throw error;
2661
+ return {
2662
+ delivery: winner,
2663
+ created: false
2664
+ };
2665
+ }
2666
+ });
2667
+ }
2668
+ /** Lease and deliver due rows; returns what happened to each leased row. */
2669
+ async dispatchDue(options = {}) {
2670
+ const limit = Math.min(options.limit ?? DEFAULT_LIMIT$1, MAX_LIMIT$1);
2671
+ if (!Number.isInteger(limit) || limit < 1) throw new Error("Notification dispatch limit must be a positive integer.");
2672
+ const candidates = await this.findDue(options.tenantIds, limit);
2673
+ const report = {
2674
+ leased: 0,
2675
+ outcomes: []
2676
+ };
2677
+ for (const candidate of candidates) {
2678
+ if (report.leased >= limit) break;
2679
+ const outcome = await withTenant({ tenantId: candidate.tenantId }, () => this.dispatchOne(candidate));
2680
+ if (!outcome) continue;
2681
+ report.leased += 1;
2682
+ report.outcomes.push({
2683
+ deliveryId: candidate.id,
2684
+ tenantId: candidate.tenantId,
2685
+ outcome
2686
+ });
2687
+ }
2688
+ return report;
2689
+ }
2690
+ /** Deliveries waiting for a reconciliation decision, oldest first. */
2691
+ async listUncertain(options) {
2692
+ const tenantId = requireText$1(options.tenantId, "tenant");
2693
+ this.authorize(tenantId);
2694
+ return withTenant({ tenantId }, async () => (await this.collection()).list({
2695
+ where: {
2696
+ tenantId,
2697
+ status: "uncertain"
2698
+ },
2699
+ orderBy: "handedOffAt ASC",
2700
+ limit: Math.min(options.limit ?? DEFAULT_LIMIT$1, MAX_LIMIT$1)
2701
+ }));
2702
+ }
2703
+ /**
2704
+ * Settle an `uncertain` delivery after checking the provider out of band:
2705
+ * `sent` (it arrived), `failed` (give up), or `retry` (send again,
2706
+ * accepting the risk of a duplicate; a message left claimed by a dead
2707
+ * sender is released first). Returns false when the row is not uncertain
2708
+ * any more, or for `retry` when the message is recorded as sent.
2709
+ */
2710
+ async reconcile(input) {
2711
+ const tenantId = requireText$1(input.tenantId, "tenant");
2712
+ this.authorize(tenantId);
2713
+ if (!UUID_PATTERN$1.test(input.deliveryId ?? "")) return false;
2714
+ return withTenant({ tenantId }, async () => {
2715
+ const delivery = (await (await this.collection()).list({
2716
+ where: {
2717
+ tenantId,
2718
+ id: input.deliveryId
2719
+ },
2720
+ limit: 1
2721
+ }))[0];
2722
+ if (delivery?.status !== "uncertain") return false;
2723
+ const note = input.note?.trim();
2724
+ const now = this.now();
2725
+ switch (input.outcome) {
2726
+ case "sent": return delivery.transition({
2727
+ status: "sent",
2728
+ deliveredAt: now,
2729
+ ...note ? { lastError: note } : {}
2730
+ });
2731
+ case "failed": return delivery.transition({
2732
+ status: "failed",
2733
+ lastError: note || delivery.lastError
2734
+ });
2735
+ case "retry": {
2736
+ const message = delivery.messageId ? await this.loadMessage(delivery.messageId) : null;
2737
+ if (message?.sendStatus === "sent") return false;
2738
+ if (message?.sendStatus === "sending" && !await message.abandonSendAttempt(note || "Released for an operator-approved retry.")) return false;
2739
+ return delivery.transition({
2740
+ status: "pending",
2741
+ nextAttemptAt: now,
2742
+ handedOffAt: null,
2743
+ attempts: Math.min(delivery.attempts, delivery.maxAttempts - 1),
2744
+ ...note ? { lastError: note } : {}
2745
+ });
2746
+ }
2747
+ default: throw new Error(`Unknown reconcile outcome '${input.outcome}'.`);
2748
+ }
2749
+ });
2750
+ }
2751
+ async requireSenderAccount(tenantId) {
2752
+ const accountId = (await this.options.resolveSenderAccountId({ tenantId }))?.trim();
2753
+ const account = accountId ? await (await AccountCollection.create({ db: this.options.db })).get({ id: accountId }) : null;
2754
+ if (!account || !(account instanceof EmailAccount) || account.tenantId !== tenantId || !account.isActive) throw new NotificationOutboxDeniedError("sender", "No active email sender account is configured for this tenant.");
2755
+ return account.id;
2756
+ }
2757
+ async isSuppressed(request) {
2758
+ return Boolean(await this.options.isSuppressed?.(request));
2759
+ }
2760
+ async findDue(tenantIds, limit) {
2761
+ const now = this.now();
2762
+ const read = async (tenantFilter) => {
2763
+ const deliveries = await this.collection();
2764
+ const [due, expired] = await Promise.all([deliveries.list({
2765
+ where: {
2766
+ ...tenantFilter,
2767
+ status: "pending",
2768
+ "nextAttemptAt <=": now
2769
+ },
2770
+ orderBy: "nextAttemptAt ASC",
2771
+ limit
2772
+ }), deliveries.list({
2773
+ where: {
2774
+ ...tenantFilter,
2775
+ status: "sending",
2776
+ "leaseExpiresAt <=": now
2777
+ },
2778
+ orderBy: "leaseExpiresAt ASC",
2779
+ limit
2780
+ })]);
2781
+ return [...expired, ...due];
2782
+ };
2783
+ return (tenantIds === void 0 ? await withSystemContext(() => read({})) : (await Promise.all([...new Set(tenantIds.filter(Boolean))].map((tenantId) => withTenant({ tenantId }, () => read({ tenantId }))))).flat()).slice(0, limit * 2);
2784
+ }
2785
+ /** Lease one candidate and carry it to an outcome; null when not leased. */
2786
+ async dispatchOne(candidate) {
2787
+ const delivery = (await (await this.collection()).list({
2788
+ where: {
2789
+ tenantId: candidate.tenantId,
2790
+ id: candidate.id
2791
+ },
2792
+ limit: 1
2793
+ }))[0];
2794
+ if (!delivery) return null;
2795
+ const now = this.now();
2796
+ const recovering = delivery.status === "sending" && delivery.leaseExpiresAt !== null && delivery.leaseExpiresAt.getTime() <= now.getTime();
2797
+ const due = delivery.status === "pending" && delivery.nextAttemptAt.getTime() <= now.getTime();
2798
+ if (!recovering && !due) return null;
2799
+ const leaseToken = crypto.randomUUID();
2800
+ const leaseExpiresAt = new Date(now.getTime() + this.leaseMs);
2801
+ const priorHandoff = recovering && delivery.handedOffAt !== null;
2802
+ if (!await delivery.transition({
2803
+ status: "sending",
2804
+ leaseToken,
2805
+ leaseExpiresAt,
2806
+ attempts: priorHandoff ? delivery.attempts : delivery.attempts + 1,
2807
+ ...priorHandoff ? {} : { handedOffAt: null }
2808
+ })) return null;
2809
+ try {
2810
+ if (priorHandoff) {
2811
+ const resolved = await this.recoverHandoff(delivery, leaseToken);
2812
+ if (resolved) return resolved;
2813
+ }
2814
+ if (await this.isSuppressed({
2815
+ tenantId: delivery.tenantId,
2816
+ recipientUserId: delivery.recipientUserId,
2817
+ channel: delivery.channel,
2818
+ kind: delivery.kind,
2819
+ sourceRef: delivery.sourceRef
2820
+ })) return this.settle(delivery, leaseToken, {
2821
+ status: "cancelled",
2822
+ lastError: "Suppressed by recipient preference."
2823
+ });
2824
+ const message = await this.ensureMessage(delivery, leaseToken);
2825
+ if (!message) return "lease-lost";
2826
+ if (!await delivery.transition({ handedOffAt: this.now() }, { leaseToken })) return "lease-lost";
2827
+ let outcome;
2828
+ try {
2829
+ outcome = await this.transport.send({
2830
+ delivery,
2831
+ message,
2832
+ idempotencyKey: notificationDeliveryCorrelationId(delivery.id)
2833
+ });
2834
+ } catch (error) {
2835
+ outcome = {
2836
+ status: "unknown",
2837
+ error: errorText(error)
2838
+ };
2839
+ }
2840
+ return this.applyOutcome(delivery, leaseToken, outcome);
2841
+ } catch (error) {
2842
+ if (delivery.handedOffAt !== null) throw error;
2843
+ return this.applyOutcome(delivery, leaseToken, {
2844
+ status: "rejected",
2845
+ error: errorText(error),
2846
+ retryable: true
2847
+ });
2848
+ }
2849
+ }
2850
+ async recoverHandoff(delivery, leaseToken) {
2851
+ const message = delivery.messageId ? await this.loadMessage(delivery.messageId) : null;
2852
+ let known = {
2853
+ status: "unknown",
2854
+ error: "Lease lapsed after the provider handoff; outcome unknown."
2855
+ };
2856
+ if (message && this.transport.reconcile) try {
2857
+ known = await this.transport.reconcile({
2858
+ delivery,
2859
+ message,
2860
+ idempotencyKey: notificationDeliveryCorrelationId(delivery.id)
2861
+ });
2862
+ } catch (error) {
2863
+ known = {
2864
+ status: "unknown",
2865
+ error: errorText(error)
2866
+ };
2867
+ }
2868
+ if (known.status === "not-sent") {
2869
+ if (!await delivery.transition({
2870
+ handedOffAt: null,
2871
+ attempts: delivery.attempts + 1
2872
+ }, { leaseToken })) return "lease-lost";
2873
+ return null;
2874
+ }
2875
+ if (known.status === "unknown" && this.transport.idempotent) {
2876
+ if (!await delivery.transition({
2877
+ handedOffAt: null,
2878
+ attempts: delivery.attempts + 1
2879
+ }, { leaseToken })) return "lease-lost";
2880
+ return null;
2881
+ }
2882
+ return this.applyOutcome(delivery, leaseToken, known);
2883
+ }
2884
+ async applyOutcome(delivery, leaseToken, outcome) {
2885
+ if (outcome.status === "accepted") return this.settle(delivery, leaseToken, {
2886
+ status: "sent",
2887
+ deliveredAt: this.now(),
2888
+ providerMessageId: outcome.providerMessageId ?? "",
2889
+ lastError: ""
2890
+ });
2891
+ if (outcome.status === "unknown") {
2892
+ if (this.transport.idempotent && delivery.attempts < delivery.maxAttempts) return this.scheduleRetry(delivery, leaseToken, outcome.error);
2893
+ return this.settle(delivery, leaseToken, {
2894
+ status: "uncertain",
2895
+ lastError: outcome.error
2896
+ });
2897
+ }
2898
+ if (outcome.retryable && delivery.attempts < delivery.maxAttempts) return this.scheduleRetry(delivery, leaseToken, outcome.error);
2899
+ return this.settle(delivery, leaseToken, {
2900
+ status: "failed",
2901
+ lastError: outcome.error
2902
+ });
2903
+ }
2904
+ async scheduleRetry(delivery, leaseToken, error) {
2905
+ const delay = this.retryDelayMs(delivery.attempts);
2906
+ return await this.endLease(delivery, leaseToken, {
2907
+ status: "pending",
2908
+ nextAttemptAt: new Date(this.now().getTime() + Math.max(0, delay)),
2909
+ handedOffAt: null,
2910
+ lastError: error
2911
+ }) ? "retry-scheduled" : "lease-lost";
2912
+ }
2913
+ /** End the lease in a terminal status; reports lease loss instead of overwriting. */
2914
+ async settle(delivery, leaseToken, changes) {
2915
+ return await this.endLease(delivery, leaseToken, changes) ? changes.status : "lease-lost";
2916
+ }
2917
+ endLease(delivery, leaseToken, changes) {
2918
+ return delivery.transition({
2919
+ ...changes,
2920
+ leaseToken: null,
2921
+ leaseExpiresAt: null
2922
+ }, { leaseToken });
2923
+ }
2924
+ async loadMessage(messageId) {
2925
+ return await (await MessageCollection.create({ db: this.options.db })).get({ id: messageId }) ?? null;
2926
+ }
2927
+ /**
2928
+ * The delivery's single outbound `Email`, created under the lease. Its id
2929
+ * is derived from the delivery id and inserted insert-only, so a stale
2930
+ * dispatcher that resumes after its lease was taken over hits the stored
2931
+ * row (a unique violation) instead of creating a second email.
2932
+ */
2933
+ async ensureMessage(delivery, leaseToken) {
2934
+ if (delivery.messageId) {
2935
+ const message2 = await this.loadMessage(delivery.messageId);
2936
+ if (message2) return message2;
2937
+ }
2938
+ const messageId = await notificationDeliveryMessageId(delivery.id);
2939
+ let message = await this.loadMessage(messageId);
2940
+ if (!message) {
2941
+ const body = `${delivery.textBody}
2942
+
2943
+ ${delivery.href}
2944
+ `;
2945
+ const emails = await EmailCollection.create({ db: this.options.db });
2946
+ try {
2947
+ await emails.create({
2948
+ _insertOnly: true,
2949
+ id: messageId,
2950
+ tenantId: delivery.tenantId,
2951
+ accountId: delivery.accountId,
2952
+ correlationId: notificationDeliveryCorrelationId(delivery.id),
2953
+ subject: delivery.subject,
2954
+ body,
2955
+ textBody: body,
2956
+ toAddresses: JSON.stringify([{
2957
+ address: delivery.recipientAddress,
2958
+ ...delivery.recipientName ? { name: delivery.recipientName } : {}
2959
+ }]),
2960
+ sendStatus: "pending",
2961
+ maxRetries: 0
2962
+ });
2963
+ } catch (error) {
2964
+ if (!isUniqueViolationError(error)) throw error;
2965
+ }
2966
+ message = await this.loadMessage(messageId);
2967
+ if (!message) throw new Error("The outbound email could not be stored.");
2968
+ }
2969
+ if (delivery.messageId !== message.id) {
2970
+ if (!await delivery.transition({ messageId: message.id }, { leaseToken })) return null;
2971
+ }
2972
+ return message;
2973
+ }
2974
+ };
2975
+ //#endregion
2234
2976
  //#region src/services/PersonaMessagingService.ts
2235
2977
  var PersonaMessagingService = class {
2236
2978
  constructor(options) {
@@ -2759,6 +3501,6 @@ var SlackRecipe = class extends SmrtRecipe {
2759
3501
  ensureBuiltinMessagingProvidersRegistered();
2760
3502
  ensureMessagingPermissionsRegistered();
2761
3503
  //#endregion
2762
- export { Account, AccountCollection, Attachment, AttachmentCollection, Blacklist, BlacklistCollection, Email, EmailAccount, EmailAccountCollection, EmailAttachment, EmailAttachmentCollection, EmailCollection, EmailFolder, EmailFolderCollection, EmailSender, MANAGE_MESSAGE_CREDENTIALS_PERMISSION, MANAGE_MESSAGE_ROUTES_PERMISSION, MESSAGING_PERMISSION_DEFINITIONS, MailboxRecipe, Message, MessageCollection, MessagingEndpoint, MessagingEndpointCollection, MessagingSettingsService, NotificationsRecipe, PersonaMessageRoute, PersonaMessageRouteCollection, PersonaMessagingService, SEND_MESSAGES_PERMISSION, SlackAccount, SlackMessage, SlackRecipe, SlackSender, TelegramAccount, TelegramSender, Tweet, TweetSender, TwitterAccount, USER_NOTIFICATION_SEVERITIES, UserNotification, UserNotificationCollection, UserNotificationService, Whitelist, WhitelistCollection, ZulipAccount, ZulipSender, AttachmentCollection_exports as a, assertMessagingPermission, ensureBuiltinMessagingProvidersRegistered, ensureMessagingPermissionsRegistered, getMessagingProvider, MessagingEndpointCollection_exports as i, listMessagingProviders, messagingPrincipalFromPermissions, EmailCollection_exports as n, EmailAccountCollection_exports as r, registerMessagingProvider, EmailFolderCollection_exports as t };
3504
+ export { Account, AccountCollection, Attachment, AttachmentCollection, Blacklist, BlacklistCollection, Email, EmailAccount, EmailAccountCollection, EmailAttachment, EmailAttachmentCollection, EmailCollection, EmailFolder, EmailFolderCollection, EmailSender, MANAGE_MESSAGE_CREDENTIALS_PERMISSION, MANAGE_MESSAGE_ROUTES_PERMISSION, MESSAGING_PERMISSION_DEFINITIONS, MailboxRecipe, Message, MessageCollection, MessagingEndpoint, MessagingEndpointCollection, MessagingSettingsService, NOTIFICATION_DELIVERY_CHANNELS, NotificationDelivery, NotificationDeliveryCollection, NotificationOutboxDeniedError, NotificationOutboxService, NotificationsRecipe, PersonaMessageRoute, PersonaMessageRouteCollection, PersonaMessagingService, SEND_MESSAGES_PERMISSION, SlackAccount, SlackMessage, SlackRecipe, SlackSender, TelegramAccount, TelegramSender, Tweet, TweetSender, TwitterAccount, USER_NOTIFICATION_SEVERITIES, UserNotification, UserNotificationCollection, UserNotificationService, Whitelist, WhitelistCollection, ZulipAccount, ZulipSender, AttachmentCollection_exports as a, assertMessagingPermission, buildNotificationLink, ensureBuiltinMessagingProvidersRegistered, ensureMessagingPermissionsRegistered, getMessagingProvider, MessagingEndpointCollection_exports as i, listMessagingProviders, messageSendTransport, messagingPrincipalFromPermissions, EmailCollection_exports as n, notificationDeliveryCorrelationId, notificationDeliveryMessageId, EmailAccountCollection_exports as r, registerMessagingProvider, EmailFolderCollection_exports as t };
2763
3505
 
2764
3506
  //# sourceMappingURL=index.js.map