@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/AGENTS.md CHANGED
@@ -54,6 +54,19 @@ The in-app channel: one stored row per person, with read state.
54
54
  - Hosts decide who is told (e.g. `SocialPost.createdByUserId` for a failed
55
55
  post). New table: consumers run `db:migrate`.
56
56
 
57
+ ## Notification email outbox (`NotificationDelivery`)
58
+
59
+ `NotificationOutboxService` is the external-channel counterpart of
60
+ `UserNotificationService`: `enqueueEmail()` stores one delivery per
61
+ `(tenant, recipient, channel, sourceRef)` after the host resolves the
62
+ recipient address and same-tenant sender account (callers never choose an
63
+ address); `dispatchDue()` leases due rows by compare-and-set
64
+ (`NotificationDelivery.transition()`), creates the single outbound `Email`
65
+ under the lease, and settles sent/retry/failed/uncertain. Never resend an
66
+ `uncertain` row automatically. Contract, retry, and reconciliation policy:
67
+ [agents/notification-outbox.md](agents/notification-outbox.md). New table:
68
+ consumers run `db:migrate`.
69
+
57
70
  ## Credential Security
58
71
 
59
72
  Account credentials stored via tenant-bound `credentialSecretId` →
@@ -63,6 +76,10 @@ directly. Credential descriptor schemas contain field definitions, never values.
63
76
  ## Send Lifecycle
64
77
 
65
78
  `message.send()`: resolves account → creates provider sender → updates sendStatus. Retry support with `maxRetries` budget.
79
+ A failure the provider may still have accepted carries
80
+ `MessageSendResult.outcomeUnknown` (set by `EmailSender` unless
81
+ `isDefiniteSendRejection()` proves a refusal, and when a sender throws) and is
82
+ persisted as `metadata.sendOutcomeUnknown`; never auto-resend it.
66
83
  `EmailSender` attaches the message's stored `Attachment` rows (read through
67
84
  `Attachment.readContent()`, under the message's tenant context) plus
68
85
  `SendEmailOptions.attachments` bytes; an unreadable or file-less row fails the
package/README.md CHANGED
@@ -206,6 +206,46 @@ await notifications.markAllRead(userId, { tenantIds });
206
206
  The generated REST/MCP/CLI surfaces are off; expose host routes that pass the
207
207
  signed-in user's id. The table is new: run `db:migrate`.
208
208
 
209
+ ### Notification email outbox
210
+
211
+ `NotificationOutboxService` sends one email per event and recipient from a
212
+ durable, leased outbox. The host resolves recipients and the sender account;
213
+ callers pass a user id and a host-relative link path, never an address.
214
+
215
+ ```typescript
216
+ import { NotificationOutboxService } from '@happyvertical/smrt-messages';
217
+ import '@happyvertical/smrt-messages/providers/email'; // default transport
218
+
219
+ const outbox = new NotificationOutboxService({
220
+ db,
221
+ linkBaseUrl: 'https://app.example.com',
222
+ resolveRecipient: async ({ tenantId, recipientUserId }) =>
223
+ (await isMember(tenantId, recipientUserId))
224
+ ? { address: await emailOf(recipientUserId) }
225
+ : null, // denied
226
+ resolveSenderAccountId: ({ tenantId }) => senderAccountIdFor(tenantId),
227
+ });
228
+
229
+ // Idempotent per (tenant, recipient, email, sourceRef).
230
+ await outbox.enqueueEmail({
231
+ tenantId,
232
+ recipientUserId,
233
+ sourceRef: `project-question:${question.id}:asked`,
234
+ kind: 'project-question.asked',
235
+ subject: 'A question is waiting for you',
236
+ text: question.prompt,
237
+ path: `/portal/projects/${projectId}/questions#question-${question.id}`,
238
+ });
239
+
240
+ // From a scheduled job; safe to run concurrently and after a crash.
241
+ await outbox.dispatchDue({ limit: 25 });
242
+ ```
243
+
244
+ Outcomes the provider may have seen but did not confirm become `uncertain`
245
+ and wait for `reconcile()`. See
246
+ [agents/notification-outbox.md](agents/notification-outbox.md) for the lease,
247
+ retry, and reconciliation policy. The table is new: run `db:migrate`.
248
+
209
249
  ### Credential Security
210
250
 
211
251
  Account credentials are stored via `credentialSecretId` pointing to smrt-secrets envelope encryption. Use `account.setCredentials()` and `account.getCredentials()` -- never store passwords as plain fields.
@@ -0,0 +1,116 @@
1
+ # Notification email outbox (#4237)
2
+
3
+ `NotificationOutboxService` turns a host event into at most one outbound email
4
+ per recipient and delivers it from a crash-safe, leased outbox. It pairs with
5
+ `UserNotificationService`: hosts that tell someone in-app and by email pass the
6
+ same `sourceRef` to both.
7
+
8
+ Source: `src/models/NotificationDelivery.ts`,
9
+ `src/services/NotificationOutboxService.ts`,
10
+ tests `src/__tests__/notification-outbox*.test.ts`.
11
+
12
+ ## Enqueue
13
+
14
+ `enqueueEmail({ tenantId, recipientUserId, sourceRef, kind, subject, text, path, notBefore? })`
15
+ stores one `NotificationDelivery` (`notification_deliveries`) per
16
+ `(tenant_id, recipient_user_id, channel, source_ref)`, the conflict identity. A
17
+ repeat (or a concurrent duplicate that loses the insert) returns the stored row
18
+ untouched with `created: false`. Encode each distinct event in `sourceRef`, e.g.
19
+ `project-question:<id>:asked` and `project-question:<id>:reminder:<n>`.
20
+
21
+ Host authorization, all checked before anything is stored:
22
+
23
+ - `resolveRecipient({ tenantId, recipientUserId, channel })` returns the address
24
+ the host allows for that user in that tenant, or `null` (denied with
25
+ `NotificationOutboxDeniedError`, reason `recipient`). Callers never pass an
26
+ address; the resolved one is snapshotted on the row.
27
+ - `resolveSenderAccountId({ tenantId })` names an active `EmailAccount` owned by
28
+ the same tenant (reason `sender` otherwise). Configuring that account is
29
+ deployment setup; the outbox has no mail transport of its own.
30
+ - With `principal`, enqueue, `listUncertain`, and `reconcile` require
31
+ `messages.send` in the tenant (reason `permission`). Omit it only for trusted
32
+ system code that authorizes itself.
33
+ - `isSuppressed(...)` (optional) is the opt-out hook. A suppressed enqueue stores
34
+ a `cancelled` row (so the event stays deduplicated); suppression at dispatch
35
+ cancels before handoff. The package has no preference store of its own.
36
+
37
+ Deep links: `path` must be host-relative (`/portal/...`); it is joined to
38
+ `linkBaseUrl` (absolute http(s), no credentials/query/fragment, validated at
39
+ construction) and must stay on that origin — `//host`, backslashes, whitespace,
40
+ and control characters are refused (reason `link`). The link is stored on the
41
+ row, so a duplicate enqueue keeps the first link. Links carry no tokens: they
42
+ lead to the host's normal authenticated route. Subjects must be one line.
43
+
44
+ ## Dispatch
45
+
46
+ `dispatchDue({ tenantIds?, limit? })` (default 25, max 200) leases due rows —
47
+ `pending` with `nextAttemptAt <= now`, and `sending` rows whose lease lapsed —
48
+ for the given tenants, or every tenant (discovered under `withSystemContext`)
49
+ when `tenantIds` is omitted. Each row is processed under `withTenant(row.tenantId)`.
50
+ Run it from a scheduled job; several workers may run it concurrently.
51
+
52
+ Every state change is `NotificationDelivery.transition()`: a compare-and-set on
53
+ `stateVersion` (and the lease token for a lease holder) that writes a unique
54
+ `transitionId` and re-reads the row to prove the write won. PostgreSQL and
55
+ remote LibSQL use one conditional `UPDATE`; local embedded engines use the
56
+ process write queue, like `Message.send()`'s claim.
57
+
58
+ Per row:
59
+
60
+ 1. Acquire: `status = sending`, new `leaseToken`, `leaseExpiresAt = now + leaseMs`
61
+ (default 5 min; must exceed the slowest provider call), `attempts + 1`.
62
+ 2. Create the single outbound `Email` and record `messageId`. Its id is
63
+ `notificationDeliveryMessageId(deliveryId)` (a name-based uuid) and it is
64
+ inserted insert-only, so a crashed or stale dispatcher finds or collides
65
+ with the stored row instead of creating a second email; its correlation id
66
+ is `notification-delivery:<deliveryId>`.
67
+ 3. Record `handedOffAt`, then call `transport.send({ delivery, message, idempotencyKey })`.
68
+ 4. Settle with the lease token: `accepted` → `sent`; `rejected` + `retryable`
69
+ → `pending` after `retryDelayMs(attempts)` (default 1 min · 2ⁿ⁻¹, capped at
70
+ 1 h) until `maxAttempts` (default 5), then `failed`; non-retryable → `failed`;
71
+ `unknown` (including a thrown send) → `uncertain`. A holder whose lease was
72
+ taken over gets `lease-lost` and writes nothing.
73
+
74
+ ## Provider idempotency and reconciliation policy
75
+
76
+ Delivery is at-most-once once a provider may have seen the message.
77
+
78
+ - A lapsed lease with no `handedOffAt` never reached the provider: it is resent.
79
+ - Ambiguous provider failures are not rejections. `EmailSender` sets
80
+ `MessageSendResult.outcomeUnknown` unless `isDefiniteSendRejection()` proves
81
+ the send was refused (an SMTP reply code, an SDK auth/validation error, a
82
+ send error that accepted no recipient, or a connection that was never made);
83
+ `Message.send()` sets it when a sender throws. `Message` persists the marker
84
+ in `metadata.sendOutcomeUnknown` with the `failed` status
85
+ (`Message.isSendOutcomeUnknown()`), and `messageSendTransport` maps it to
86
+ `unknown`, so the delivery becomes `uncertain` instead of being retried.
87
+ - A lapsed lease after `handedOffAt` asks `transport.reconcile()`. The default
88
+ `messageSendTransport` reads the canonical `Message.sendStatus`: `sent` →
89
+ delivery `sent` (no resend), `failed` → normal retry (or `uncertain` when the
90
+ failure carries the outcome-unknown marker), `draft`/`pending` → resend,
91
+ anything else (e.g. still `sending`) → `uncertain`.
92
+ - `uncertain` rows are never resent automatically. Operators list them with
93
+ `listUncertain({ tenantId })` and settle them with
94
+ `reconcile({ tenantId, deliveryId, outcome: 'sent' | 'failed' | 'retry', note? })`
95
+ after checking the provider; `retry` accepts the risk of a duplicate. It
96
+ first releases a message a dead sender left `sending`
97
+ (`Message.abandonSendAttempt()`, a compare-and-set to `failed`) so the next
98
+ lease can send it, and refuses when the message is recorded as `sent`.
99
+ - A transport with `idempotent: true` declares that its provider deduplicates on
100
+ `idempotencyKey` (stable per delivery); unknown outcomes are then retried
101
+ instead of parked. SMTP and the current SDK email providers take no key, so
102
+ `messageSendTransport` is not idempotent.
103
+
104
+ ## Transport seam
105
+
106
+ `NotificationTransport { name; idempotent?; send(request); reconcile?(request) }`.
107
+ The default delivers through `Message.send()` → account → registered provider
108
+ sender (import `@happyvertical/smrt-messages/providers/email` at server
109
+ startup). Tests inject a fake; nothing in the outbox reaches a provider SDK.
110
+
111
+ ## Schema
112
+
113
+ New table `notification_deliveries` (tenant-required, generated REST/MCP/CLI
114
+ off): consumers run `db:migrate`. `recipient_user_id` is a cross-package uuid
115
+ ref to `smrt-users:User`; `account_id`/`message_id` are FKs to `accounts` and
116
+ `messages`.
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import "./__smrt-register__-DwNoP4GB.js";
2
+ import "./__smrt-register__-CaGLtiEU.js";
3
3
  import { SmrtCollection, SmrtObject, field, smrt } from "@happyvertical/smrt-core";
4
4
  import { TenantScoped, getCurrentTenant, queryGlobal, queryWithGlobals, tenantId, withTenant } from "@happyvertical/smrt-tenancy";
5
5
  //#region src/models/Account.ts
@@ -255,4 +255,4 @@ var AccountCollection = class extends SmrtCollection {
255
255
  //#endregion
256
256
  export { AccountCollection_exports as n, Account as r, AccountCollection as t };
257
257
 
258
- //# sourceMappingURL=AccountCollection-O-zbituV.js.map
258
+ //# sourceMappingURL=AccountCollection-By48bdwt.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"AccountCollection-O-zbituV.js","names":["tenantId"],"sources":["../../src/models/Account.ts","../../src/collections/AccountCollection.ts"],"sourcesContent":["/**\n * Account model - Base class for all messaging account types (STI)\n *\n * Common fields shared across email, Slack, Twitter accounts, etc.\n */\n\n// Register the package manifest before this module's @smrt() decorator\n// runs, whichever chunk the library build places it in (#3098).\nimport '../__smrt-register__.js';\nimport { field, SmrtObject, smrt } from '@happyvertical/smrt-core';\nimport {\n getCurrentTenant,\n TenantScoped,\n tenantId,\n withTenant,\n} from '@happyvertical/smrt-tenancy';\nimport type { AccountOptions, MessageSenderInterface } from '../types';\n\n@TenantScoped({ mode: 'optional' })\n@smrt({\n tableStrategy: 'sti',\n api: { include: ['list', 'get'] },\n mcp: { include: ['list', 'get'] },\n cli: { include: ['list', 'get'] },\n})\nexport class Account extends SmrtObject { static __smrtPackage__ = \"@happyvertical/smrt-messages\";\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n @field({\n description: 'The display name for this connected messaging account.',\n })\n name = '';\n @field({\n description: 'The service that connects this account to your application.',\n })\n providerType = '';\n @field({\n description:\n 'The kind of messaging this account supports, such as email or chat.',\n })\n channelType = '';\n // credentialSecretId stores the Secret's NAME (keyed by name + tenant\n // context in smrt-secrets), NOT its primary-key id — see setCredentials()/\n // getCredentials() which call tenant-bound SecretService operations by name.\n // So it is deliberately NOT a @crossPackageRef id FK.\n @field({ sensitive: true, readonly: true })\n credentialSecretId: string | null = null;\n @field({ description: 'Whether this account is enabled for messaging.' })\n isActive = true;\n @field({ description: 'When this account last checked for new messages.' })\n lastSyncAt: Date | null = null;\n // JSON. Sensitive (#1540): may hold a plaintext credential fallback, so it is\n // excluded from generated API/MCP responses and rejected as a `where` filter\n // key.\n @field({ sensitive: true, readonly: true })\n settings = ''; // JSON\n /** Non-secret provider configuration such as a Zulip site URL. */\n @field({\n description:\n 'The public connection settings for this account; credentials are stored separately.',\n })\n configuration = '{}';\n\n // Timestamps\n @field({ description: 'When this account was added.' })\n createdAt = new Date();\n @field({ description: 'When this account was last changed.' })\n updatedAt = new Date();\n\n constructor(options: AccountOptions = {}) {\n super(options);\n\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.name !== undefined) this.name = options.name;\n if (options.providerType !== undefined)\n this.providerType = options.providerType;\n if (options.channelType !== undefined)\n this.channelType = options.channelType;\n if (options.credentialSecretId !== undefined)\n this.credentialSecretId = options.credentialSecretId || null;\n if (options.isActive !== undefined) this.isActive = options.isActive;\n if (options.lastSyncAt !== undefined)\n this.lastSyncAt = options.lastSyncAt || null;\n if (options.settings !== undefined) this.settings = options.settings;\n if (options.configuration !== undefined)\n this.configuration = options.configuration;\n if (options.createdAt) this.createdAt = options.createdAt;\n if (options.updatedAt) this.updatedAt = options.updatedAt;\n }\n\n /**\n * Get settings as parsed object\n */\n getSettings(): Record<string, unknown> {\n if (!this.settings) return {};\n try {\n return JSON.parse(this.settings);\n } catch {\n return {};\n }\n }\n\n /**\n * Set settings from object\n */\n setSettings(settings: Record<string, unknown>): void {\n this.settings = JSON.stringify(settings);\n }\n\n getConfiguration(): Record<string, unknown> {\n try {\n return JSON.parse(this.configuration || '{}');\n } catch {\n return {};\n }\n }\n\n setConfiguration(configuration: Record<string, unknown>): void {\n this.configuration = JSON.stringify(configuration);\n }\n\n get hasCredentials(): boolean {\n return Boolean(this.credentialSecretId || this.settings);\n }\n\n /**\n * Activate account\n */\n async activate(): Promise<void> {\n this.isActive = true;\n this.updatedAt = new Date();\n await this.save();\n }\n\n /**\n * Deactivate account\n */\n async deactivate(): Promise<void> {\n this.isActive = false;\n this.updatedAt = new Date();\n await this.save();\n }\n\n /**\n * Store credentials securely using smrt-secrets\n */\n async setCredentials(\n credentials: Record<string, unknown>,\n options: {\n description?: string;\n category?: string;\n } = {},\n ): Promise<void> {\n if (!this.id) {\n throw new Error('Account must be persisted before storing credentials.');\n }\n const { SecretService } = await import('@happyvertical/smrt-secrets');\n const secretService = await SecretService.create({ db: this.db });\n\n const secretName = `${this.channelType || 'messaging'}-account-${this.id}`;\n const secretValue = JSON.stringify(credentials);\n const tenantId = this.requireCredentialTenantId('store credentials');\n\n await secretService.storeForTenant(\n tenantId,\n this.credentialSecretId || secretName,\n secretValue,\n {\n description: options.description || `Credentials for ${this.name}`,\n category: options.category || 'messaging',\n },\n );\n\n this.credentialSecretId ||= secretName;\n this.updatedAt = new Date();\n await this.withCredentialTenantContext(() => this.save());\n }\n\n /**\n * Create a sender for this account.\n * Subclasses must override to return a concrete sender.\n */\n async createSender(): Promise<MessageSenderInterface> {\n const { getMessagingProvider } = await import('../providers.js');\n const provider = getMessagingProvider(this.providerType);\n if (!provider?.createSender) {\n throw new Error(\n `createSender() not implemented for account type '${this.providerType}'`,\n );\n }\n return provider.createSender(this);\n }\n\n /**\n * Retrieve stored credentials\n */\n async getCredentials(): Promise<Record<string, unknown> | null> {\n if (!this.credentialSecretId) {\n return this.getSettings();\n }\n\n const tenantId = this.requireCredentialTenantId('retrieve credentials');\n const { SecretService } = await import('@happyvertical/smrt-secrets');\n const secretService = await SecretService.create({ db: this.db });\n\n try {\n const secret = await secretService.retrieveForTenant(\n tenantId,\n this.credentialSecretId,\n );\n return JSON.parse(secret.value);\n } catch {\n return null;\n }\n }\n\n private getCredentialTenantId(): string | null {\n return this.tenantId || getCurrentTenant()?.tenantId || null;\n }\n\n private requireCredentialTenantId(action: string): string {\n const tenantId = this.getCredentialTenantId();\n if (!tenantId) {\n throw new Error(`Tenant context required to ${action}.`);\n }\n return tenantId;\n }\n\n private async withCredentialTenantContext<T>(\n fn: () => Promise<T>,\n ): Promise<T> {\n const tenantId = this.getCredentialTenantId();\n if (!tenantId || getCurrentTenant()?.tenantId === tenantId) return fn();\n return withTenant({ tenantId }, fn);\n }\n}\n","/**\n * AccountCollection - Unified collection for polymorphic account queries\n */\n\nimport { SmrtCollection } from '@happyvertical/smrt-core';\nimport { queryGlobal, queryWithGlobals } from '@happyvertical/smrt-tenancy';\nimport { Account } from '../models/Account';\nimport type { AccountSearchFilters } from '../types';\n\nexport class AccountCollection extends SmrtCollection<Account> {\n static readonly _itemClass = Account;\n\n /**\n * Get active accounts\n */\n async getActive(): Promise<Account[]> {\n return await this.list({ where: { isActive: true } });\n }\n\n /**\n * Get inactive accounts\n */\n async getInactive(): Promise<Account[]> {\n return await this.list({ where: { isActive: false } });\n }\n\n /**\n * Get accounts by provider type\n */\n async getByProviderType(providerType: string): Promise<Account[]> {\n return await this.list({ where: { providerType } });\n }\n\n /**\n * Get accounts by STI type.\n *\n * Accepts either the full discriminator (e.g. \"@happyvertical/smrt-messages:EmailAccount\")\n * or the short type name (e.g. \"EmailAccount\").\n */\n async getByType(accountType: string): Promise<Account[]> {\n if (accountType.includes(':') || accountType.startsWith('@')) {\n return await this.list({ where: { _meta_type: accountType } });\n }\n const allAccounts = await this.list({});\n return allAccounts.filter((a) => {\n const metaType = (a as { _meta_type?: string })._meta_type || '';\n return metaType.endsWith(`:${accountType}`);\n });\n }\n\n /**\n * Search accounts with filters\n */\n async search(\n query: string,\n filters?: AccountSearchFilters,\n ): Promise<Account[]> {\n let accounts = await this.list({});\n\n if (query) {\n const lowerQuery = query.toLowerCase();\n accounts = accounts.filter((a) =>\n a.name?.toLowerCase().includes(lowerQuery),\n );\n }\n\n if (filters) {\n if (filters.providerType) {\n accounts = accounts.filter(\n (a) => a.providerType === filters.providerType,\n );\n }\n if (filters.isActive !== undefined) {\n accounts = accounts.filter((a) => a.isActive === filters.isActive);\n }\n if (filters.accountType) {\n accounts = accounts.filter((a) => {\n const metaType = (a as { _meta_type?: string })._meta_type || '';\n return metaType.includes(filters.accountType as string);\n });\n }\n }\n\n return accounts;\n }\n\n /**\n * Get account statistics\n */\n async getStats(): Promise<{\n total: number;\n active: number;\n inactive: number;\n byType: Record<string, number>;\n }> {\n const accounts = await this.list({});\n const byType: Record<string, number> = {};\n\n for (const account of accounts) {\n const metaType =\n (account as { _meta_type?: string })._meta_type || 'Unknown';\n const shortType = metaType.split(':').pop() || metaType;\n byType[shortType] = (byType[shortType] || 0) + 1;\n }\n\n return {\n total: accounts.length,\n active: accounts.filter((a) => a.isActive).length,\n inactive: accounts.filter((a) => !a.isActive).length,\n byType,\n };\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Tenant Helper Methods\n // ─────────────────────────────────────────────────────────────────────────\n\n async findByTenant(tenantId: string): Promise<Account[]> {\n return this.list({ where: { tenantId } });\n }\n\n // Account is the @TenantScoped STI base — see MessageCollection. Route through\n // the raw helpers; no `_meta_type` so the base returns ALL account subtypes.\n async findGlobal(): Promise<Account[]> {\n return queryGlobal<Account>(this);\n }\n\n async findWithGlobals(tenantId: string): Promise<Account[]> {\n return queryWithGlobals<Account>(this, tenantId, 'Account.findWithGlobals');\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"AccountCollection-By48bdwt.js","names":["tenantId"],"sources":["../../src/models/Account.ts","../../src/collections/AccountCollection.ts"],"sourcesContent":["/**\n * Account model - Base class for all messaging account types (STI)\n *\n * Common fields shared across email, Slack, Twitter accounts, etc.\n */\n\n// Register the package manifest before this module's @smrt() decorator\n// runs, whichever chunk the library build places it in (#3098).\nimport '../__smrt-register__.js';\nimport { field, SmrtObject, smrt } from '@happyvertical/smrt-core';\nimport {\n getCurrentTenant,\n TenantScoped,\n tenantId,\n withTenant,\n} from '@happyvertical/smrt-tenancy';\nimport type { AccountOptions, MessageSenderInterface } from '../types';\n\n@TenantScoped({ mode: 'optional' })\n@smrt({\n tableStrategy: 'sti',\n api: { include: ['list', 'get'] },\n mcp: { include: ['list', 'get'] },\n cli: { include: ['list', 'get'] },\n})\nexport class Account extends SmrtObject { static __smrtPackage__ = \"@happyvertical/smrt-messages\";\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n @field({\n description: 'The display name for this connected messaging account.',\n })\n name = '';\n @field({\n description: 'The service that connects this account to your application.',\n })\n providerType = '';\n @field({\n description:\n 'The kind of messaging this account supports, such as email or chat.',\n })\n channelType = '';\n // credentialSecretId stores the Secret's NAME (keyed by name + tenant\n // context in smrt-secrets), NOT its primary-key id — see setCredentials()/\n // getCredentials() which call tenant-bound SecretService operations by name.\n // So it is deliberately NOT a @crossPackageRef id FK.\n @field({ sensitive: true, readonly: true })\n credentialSecretId: string | null = null;\n @field({ description: 'Whether this account is enabled for messaging.' })\n isActive = true;\n @field({ description: 'When this account last checked for new messages.' })\n lastSyncAt: Date | null = null;\n // JSON. Sensitive (#1540): may hold a plaintext credential fallback, so it is\n // excluded from generated API/MCP responses and rejected as a `where` filter\n // key.\n @field({ sensitive: true, readonly: true })\n settings = ''; // JSON\n /** Non-secret provider configuration such as a Zulip site URL. */\n @field({\n description:\n 'The public connection settings for this account; credentials are stored separately.',\n })\n configuration = '{}';\n\n // Timestamps\n @field({ description: 'When this account was added.' })\n createdAt = new Date();\n @field({ description: 'When this account was last changed.' })\n updatedAt = new Date();\n\n constructor(options: AccountOptions = {}) {\n super(options);\n\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.name !== undefined) this.name = options.name;\n if (options.providerType !== undefined)\n this.providerType = options.providerType;\n if (options.channelType !== undefined)\n this.channelType = options.channelType;\n if (options.credentialSecretId !== undefined)\n this.credentialSecretId = options.credentialSecretId || null;\n if (options.isActive !== undefined) this.isActive = options.isActive;\n if (options.lastSyncAt !== undefined)\n this.lastSyncAt = options.lastSyncAt || null;\n if (options.settings !== undefined) this.settings = options.settings;\n if (options.configuration !== undefined)\n this.configuration = options.configuration;\n if (options.createdAt) this.createdAt = options.createdAt;\n if (options.updatedAt) this.updatedAt = options.updatedAt;\n }\n\n /**\n * Get settings as parsed object\n */\n getSettings(): Record<string, unknown> {\n if (!this.settings) return {};\n try {\n return JSON.parse(this.settings);\n } catch {\n return {};\n }\n }\n\n /**\n * Set settings from object\n */\n setSettings(settings: Record<string, unknown>): void {\n this.settings = JSON.stringify(settings);\n }\n\n getConfiguration(): Record<string, unknown> {\n try {\n return JSON.parse(this.configuration || '{}');\n } catch {\n return {};\n }\n }\n\n setConfiguration(configuration: Record<string, unknown>): void {\n this.configuration = JSON.stringify(configuration);\n }\n\n get hasCredentials(): boolean {\n return Boolean(this.credentialSecretId || this.settings);\n }\n\n /**\n * Activate account\n */\n async activate(): Promise<void> {\n this.isActive = true;\n this.updatedAt = new Date();\n await this.save();\n }\n\n /**\n * Deactivate account\n */\n async deactivate(): Promise<void> {\n this.isActive = false;\n this.updatedAt = new Date();\n await this.save();\n }\n\n /**\n * Store credentials securely using smrt-secrets\n */\n async setCredentials(\n credentials: Record<string, unknown>,\n options: {\n description?: string;\n category?: string;\n } = {},\n ): Promise<void> {\n if (!this.id) {\n throw new Error('Account must be persisted before storing credentials.');\n }\n const { SecretService } = await import('@happyvertical/smrt-secrets');\n const secretService = await SecretService.create({ db: this.db });\n\n const secretName = `${this.channelType || 'messaging'}-account-${this.id}`;\n const secretValue = JSON.stringify(credentials);\n const tenantId = this.requireCredentialTenantId('store credentials');\n\n await secretService.storeForTenant(\n tenantId,\n this.credentialSecretId || secretName,\n secretValue,\n {\n description: options.description || `Credentials for ${this.name}`,\n category: options.category || 'messaging',\n },\n );\n\n this.credentialSecretId ||= secretName;\n this.updatedAt = new Date();\n await this.withCredentialTenantContext(() => this.save());\n }\n\n /**\n * Create a sender for this account.\n * Subclasses must override to return a concrete sender.\n */\n async createSender(): Promise<MessageSenderInterface> {\n const { getMessagingProvider } = await import('../providers.js');\n const provider = getMessagingProvider(this.providerType);\n if (!provider?.createSender) {\n throw new Error(\n `createSender() not implemented for account type '${this.providerType}'`,\n );\n }\n return provider.createSender(this);\n }\n\n /**\n * Retrieve stored credentials\n */\n async getCredentials(): Promise<Record<string, unknown> | null> {\n if (!this.credentialSecretId) {\n return this.getSettings();\n }\n\n const tenantId = this.requireCredentialTenantId('retrieve credentials');\n const { SecretService } = await import('@happyvertical/smrt-secrets');\n const secretService = await SecretService.create({ db: this.db });\n\n try {\n const secret = await secretService.retrieveForTenant(\n tenantId,\n this.credentialSecretId,\n );\n return JSON.parse(secret.value);\n } catch {\n return null;\n }\n }\n\n private getCredentialTenantId(): string | null {\n return this.tenantId || getCurrentTenant()?.tenantId || null;\n }\n\n private requireCredentialTenantId(action: string): string {\n const tenantId = this.getCredentialTenantId();\n if (!tenantId) {\n throw new Error(`Tenant context required to ${action}.`);\n }\n return tenantId;\n }\n\n private async withCredentialTenantContext<T>(\n fn: () => Promise<T>,\n ): Promise<T> {\n const tenantId = this.getCredentialTenantId();\n if (!tenantId || getCurrentTenant()?.tenantId === tenantId) return fn();\n return withTenant({ tenantId }, fn);\n }\n}\n","/**\n * AccountCollection - Unified collection for polymorphic account queries\n */\n\nimport { SmrtCollection } from '@happyvertical/smrt-core';\nimport { queryGlobal, queryWithGlobals } from '@happyvertical/smrt-tenancy';\nimport { Account } from '../models/Account';\nimport type { AccountSearchFilters } from '../types';\n\nexport class AccountCollection extends SmrtCollection<Account> {\n static readonly _itemClass = Account;\n\n /**\n * Get active accounts\n */\n async getActive(): Promise<Account[]> {\n return await this.list({ where: { isActive: true } });\n }\n\n /**\n * Get inactive accounts\n */\n async getInactive(): Promise<Account[]> {\n return await this.list({ where: { isActive: false } });\n }\n\n /**\n * Get accounts by provider type\n */\n async getByProviderType(providerType: string): Promise<Account[]> {\n return await this.list({ where: { providerType } });\n }\n\n /**\n * Get accounts by STI type.\n *\n * Accepts either the full discriminator (e.g. \"@happyvertical/smrt-messages:EmailAccount\")\n * or the short type name (e.g. \"EmailAccount\").\n */\n async getByType(accountType: string): Promise<Account[]> {\n if (accountType.includes(':') || accountType.startsWith('@')) {\n return await this.list({ where: { _meta_type: accountType } });\n }\n const allAccounts = await this.list({});\n return allAccounts.filter((a) => {\n const metaType = (a as { _meta_type?: string })._meta_type || '';\n return metaType.endsWith(`:${accountType}`);\n });\n }\n\n /**\n * Search accounts with filters\n */\n async search(\n query: string,\n filters?: AccountSearchFilters,\n ): Promise<Account[]> {\n let accounts = await this.list({});\n\n if (query) {\n const lowerQuery = query.toLowerCase();\n accounts = accounts.filter((a) =>\n a.name?.toLowerCase().includes(lowerQuery),\n );\n }\n\n if (filters) {\n if (filters.providerType) {\n accounts = accounts.filter(\n (a) => a.providerType === filters.providerType,\n );\n }\n if (filters.isActive !== undefined) {\n accounts = accounts.filter((a) => a.isActive === filters.isActive);\n }\n if (filters.accountType) {\n accounts = accounts.filter((a) => {\n const metaType = (a as { _meta_type?: string })._meta_type || '';\n return metaType.includes(filters.accountType as string);\n });\n }\n }\n\n return accounts;\n }\n\n /**\n * Get account statistics\n */\n async getStats(): Promise<{\n total: number;\n active: number;\n inactive: number;\n byType: Record<string, number>;\n }> {\n const accounts = await this.list({});\n const byType: Record<string, number> = {};\n\n for (const account of accounts) {\n const metaType =\n (account as { _meta_type?: string })._meta_type || 'Unknown';\n const shortType = metaType.split(':').pop() || metaType;\n byType[shortType] = (byType[shortType] || 0) + 1;\n }\n\n return {\n total: accounts.length,\n active: accounts.filter((a) => a.isActive).length,\n inactive: accounts.filter((a) => !a.isActive).length,\n byType,\n };\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Tenant Helper Methods\n // ─────────────────────────────────────────────────────────────────────────\n\n async findByTenant(tenantId: string): Promise<Account[]> {\n return this.list({ where: { tenantId } });\n }\n\n // Account is the @TenantScoped STI base — see MessageCollection. Route through\n // the raw helpers; no `_meta_type` so the base returns ALL account subtypes.\n async findGlobal(): Promise<Account[]> {\n return queryGlobal<Account>(this);\n }\n\n async findWithGlobals(tenantId: string): Promise<Account[]> {\n return queryWithGlobals<Account>(this, tenantId, 'Account.findWithGlobals');\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAyBO,IAAM,UAAN,cAAsB,WAAW;CAEtC,WAA0B;CAK1B,OAAO;CAIP,eAAe;CAKf,cAAc;CAMd,qBAAoC;CAEpC,WAAW;CAEX,aAA0B;CAK1B,WAAW;CAMX,gBAAgB;CAIhB,4BAAY,IAAI,KAAK;CAErB,4BAAY,IAAI,KAAK;CAErB,YAAY,UAA0B,CAAC,GAAG;EACxC,MAAM,OAAO;EAEb,IAAI,QAAQ,aAAa,KAAA,GAAW,KAAK,WAAW,QAAQ;EAC5D,IAAI,QAAQ,SAAS,KAAA,GAAW,KAAK,OAAO,QAAQ;EACpD,IAAI,QAAQ,iBAAiB,KAAA,GAC3B,KAAK,eAAe,QAAQ;EAC9B,IAAI,QAAQ,gBAAgB,KAAA,GAC1B,KAAK,cAAc,QAAQ;EAC7B,IAAI,QAAQ,uBAAuB,KAAA,GACjC,KAAK,qBAAqB,QAAQ,sBAAsB;EAC1D,IAAI,QAAQ,aAAa,KAAA,GAAW,KAAK,WAAW,QAAQ;EAC5D,IAAI,QAAQ,eAAe,KAAA,GACzB,KAAK,aAAa,QAAQ,cAAc;EAC1C,IAAI,QAAQ,aAAa,KAAA,GAAW,KAAK,WAAW,QAAQ;EAC5D,IAAI,QAAQ,kBAAkB,KAAA,GAC5B,KAAK,gBAAgB,QAAQ;EAC/B,IAAI,QAAQ,WAAW,KAAK,YAAY,QAAQ;EAChD,IAAI,QAAQ,WAAW,KAAK,YAAY,QAAQ;CAClD;;;;CAKA,cAAuC;EACrC,IAAI,CAAC,KAAK,UAAU,OAAO,CAAC;EAC5B,IAAI;GACF,OAAO,KAAK,MAAM,KAAK,QAAQ;EACjC,QAAQ;GACN,OAAO,CAAC;EACV;CACF;;;;CAKA,YAAY,UAAyC;EACnD,KAAK,WAAW,KAAK,UAAU,QAAQ;CACzC;CAEA,mBAA4C;EAC1C,IAAI;GACF,OAAO,KAAK,MAAM,KAAK,iBAAiB,IAAI;EAC9C,QAAQ;GACN,OAAO,CAAC;EACV;CACF;CAEA,iBAAiB,eAA8C;EAC7D,KAAK,gBAAgB,KAAK,UAAU,aAAa;CACnD;CAEA,IAAI,iBAA0B;EAC5B,OAAO,QAAQ,KAAK,sBAAsB,KAAK,QAAQ;CACzD;;;;CAKA,MAAM,WAA0B;EAC9B,KAAK,WAAW;EAChB,KAAK,4BAAY,IAAI,KAAK;EAC1B,MAAM,KAAK,KAAK;CAClB;;;;CAKA,MAAM,aAA4B;EAChC,KAAK,WAAW;EAChB,KAAK,4BAAY,IAAI,KAAK;EAC1B,MAAM,KAAK,KAAK;CAClB;;;;CAKA,MAAM,eACJ,aACA,UAGI,CAAC,GACU;EACf,IAAI,CAAC,KAAK,IACR,MAAM,IAAI,MAAM,uDAAuD;EAEzE,MAAM,EAAE,kBAAkB,MAAM,OAAO;EACvC,MAAM,gBAAgB,MAAM,cAAc,OAAO,EAAE,IAAI,KAAK,GAAG,CAAC;EAEhE,MAAM,aAAa,GAAG,KAAK,eAAe,YAAW,WAAY,KAAK;EACtE,MAAM,cAAc,KAAK,UAAU,WAAW;EAC9C,MAAMA,YAAW,KAAK,0BAA0B,mBAAmB;EAEnE,MAAM,cAAc,eAClBA,WACA,KAAK,sBAAsB,YAC3B,aACA;GACE,aAAa,QAAQ,eAAe,mBAAmB,KAAK;GAC5D,UAAU,QAAQ,YAAY;EAChC,CACF;EAEA,KAAK,uBAAuB;EAC5B,KAAK,4BAAY,IAAI,KAAK;EAC1B,MAAM,KAAK,kCAAkC,KAAK,KAAK,CAAC;CAC1D;;;;;CAMA,MAAM,eAAgD;EACpD,MAAM,EAAE,yBAAyB,MAAM,OAAO,0BAAA,CAAA,MAAA,MAAA,EAAA,CAAA;EAC9C,MAAM,WAAW,qBAAqB,KAAK,YAAY;EACvD,IAAI,CAAC,UAAU,cACb,MAAM,IAAI,MACR,oDAAoD,KAAK,aAAY,EACvE;EAEF,OAAO,SAAS,aAAa,IAAI;CACnC;;;;CAKA,MAAM,iBAA0D;EAC9D,IAAI,CAAC,KAAK,oBACR,OAAO,KAAK,YAAY;EAG1B,MAAMA,YAAW,KAAK,0BAA0B,sBAAsB;EACtE,MAAM,EAAE,kBAAkB,MAAM,OAAO;EACvC,MAAM,gBAAgB,MAAM,cAAc,OAAO,EAAE,IAAI,KAAK,GAAG,CAAC;EAEhE,IAAI;GACF,MAAM,SAAS,MAAM,cAAc,kBACjCA,WACA,KAAK,kBACP;GACA,OAAO,KAAK,MAAM,OAAO,KAAK;EAChC,QAAQ;GACN,OAAO;EACT;CACF;CAEQ,wBAAuC;EAC7C,OAAO,KAAK,YAAY,iBAAiB,CAAA,EAAG,YAAY;CAC1D;CAEQ,0BAA0B,QAAwB;EACxD,MAAMA,YAAW,KAAK,sBAAsB;EAC5C,IAAI,CAACA,WACH,MAAM,IAAI,MAAM,8BAA8B,OAAM,EAAG;EAEzD,OAAOA;CACT;CAEA,MAAc,4BACZ,IACY;EACZ,MAAMA,YAAW,KAAK,sBAAsB;EAC5C,IAAI,CAACA,aAAY,iBAAiB,CAAA,EAAG,aAAaA,WAAU,OAAO,GAAG;EACtE,OAAO,WAAW,EAAE,UAAAA,UAAS,GAAG,EAAE;CACpC;AACF;AAnN0C,cAA7B,SAAoC,mBAAkB,8BAAA;AAEjE,gBAAA,CADC,SAAS,EAAE,UAAU,KAAK,CAAC,CAAA,GADjB,QAEX,WAAA,YAAA,CAAA;AAKA,gBAAA,CAHC,MAAM,EACL,aAAa,yDACf,CAAC,CAAA,GANU,QAOX,WAAA,QAAA,CAAA;AAIA,gBAAA,CAHC,MAAM,EACL,aAAa,8DACf,CAAC,CAAA,GAVU,QAWX,WAAA,gBAAA,CAAA;AAKA,gBAAA,CAJC,MAAM,EACL,aACE,sEACJ,CAAC,CAAA,GAfU,QAgBX,WAAA,eAAA,CAAA;AAMA,gBAAA,CADC,MAAM;CAAE,WAAW;CAAM,UAAU;AAAK,CAAC,CAAA,GArB/B,QAsBX,WAAA,sBAAA,CAAA;AAEA,gBAAA,CADC,MAAM,EAAE,aAAa,iDAAiD,CAAC,CAAA,GAvB7D,QAwBX,WAAA,YAAA,CAAA;AAEA,gBAAA,CADC,MAAM,EAAE,aAAa,mDAAmD,CAAC,CAAA,GAzB/D,QA0BX,WAAA,cAAA,CAAA;AAKA,gBAAA,CADC,MAAM;CAAE,WAAW;CAAM,UAAU;AAAK,CAAC,CAAA,GA9B/B,QA+BX,WAAA,YAAA,CAAA;AAMA,gBAAA,CAJC,MAAM,EACL,aACE,sFACJ,CAAC,CAAA,GApCU,QAqCX,WAAA,iBAAA,CAAA;AAIA,gBAAA,CADC,MAAM,EAAE,aAAa,+BAA+B,CAAC,CAAA,GAxC3C,QAyCX,WAAA,aAAA,CAAA;AAEA,gBAAA,CADC,MAAM,EAAE,aAAa,sCAAsC,CAAC,CAAA,GA1ClD,QA2CX,WAAA,aAAA,CAAA;AA3CW,UAAN,gBAAA,CAPN,aAAa,EAAE,MAAM,WAAW,CAAC,GACjC,KAAK;CACJ,eAAe;CACf,KAAK,EAAE,SAAS,CAAC,QAAQ,KAAK,EAAE;CAChC,KAAK,EAAE,SAAS,CAAC,QAAQ,KAAK,EAAE;CAChC,KAAK,EAAE,SAAS,CAAC,QAAQ,KAAK,EAAE;AAClC,CAAC,CAAA,GACY,OAAA;;;;AChBN,IAAM,oBAAN,cAAgC,eAAwB;CAC7D,OAAgB,aAAa;;;;CAK7B,MAAM,YAAgC;EACpC,OAAO,MAAM,KAAK,KAAK,EAAE,OAAO,EAAE,UAAU,KAAK,EAAE,CAAC;CACtD;;;;CAKA,MAAM,cAAkC;EACtC,OAAO,MAAM,KAAK,KAAK,EAAE,OAAO,EAAE,UAAU,MAAM,EAAE,CAAC;CACvD;;;;CAKA,MAAM,kBAAkB,cAA0C;EAChE,OAAO,MAAM,KAAK,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC;CACpD;;;;;;;CAQA,MAAM,UAAU,aAAyC;EACvD,IAAI,YAAY,SAAS,GAAG,KAAK,YAAY,WAAW,GAAG,GACzD,OAAO,MAAM,KAAK,KAAK,EAAE,OAAO,EAAE,YAAY,YAAY,EAAE,CAAC;EAG/D,QAAO,MADmB,KAAK,KAAK,CAAC,CAAC,EAAA,CACnB,QAAQ,MAAM;GAE/B,QADkB,EAA8B,cAAc,GAAA,CAC9C,SAAS,IAAI,aAAa;EAC5C,CAAC;CACH;;;;CAKA,MAAM,OACJ,OACA,SACoB;EACpB,IAAI,WAAW,MAAM,KAAK,KAAK,CAAC,CAAC;EAEjC,IAAI,OAAO;GACT,MAAM,aAAa,MAAM,YAAY;GACrC,WAAW,SAAS,QAAQ,MAC1B,EAAE,MAAM,YAAY,CAAA,CAAE,SAAS,UAAU,CAC3C;EACF;EAEA,IAAI,SAAS;GACX,IAAI,QAAQ,cACV,WAAW,SAAS,QACjB,MAAM,EAAE,iBAAiB,QAAQ,YACpC;GAEF,IAAI,QAAQ,aAAa,KAAA,GACvB,WAAW,SAAS,QAAQ,MAAM,EAAE,aAAa,QAAQ,QAAQ;GAEnE,IAAI,QAAQ,aACV,WAAW,SAAS,QAAQ,MAAM;IAEhC,QADkB,EAA8B,cAAc,GAAA,CAC9C,SAAS,QAAQ,WAAqB;GACxD,CAAC;EAEL;EAEA,OAAO;CACT;;;;CAKA,MAAM,WAKH;EACD,MAAM,WAAW,MAAM,KAAK,KAAK,CAAC,CAAC;EACnC,MAAM,SAAiC,CAAC;EAExC,KAAA,MAAW,WAAW,UAAU;GAC9B,MAAM,WACH,QAAoC,cAAc;GACrD,MAAM,YAAY,SAAS,MAAM,GAAG,CAAA,CAAE,IAAI,KAAK;GAC/C,OAAO,cAAc,OAAO,cAAc,KAAK;EACjD;EAEA,OAAO;GACL,OAAO,SAAS;GAChB,QAAQ,SAAS,QAAQ,MAAM,EAAE,QAAQ,CAAA,CAAE;GAC3C,UAAU,SAAS,QAAQ,MAAM,CAAC,EAAE,QAAQ,CAAA,CAAE;GAC9C;EACF;CACF;CAMA,MAAM,aAAa,UAAsC;EACvD,OAAO,KAAK,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC;CAC1C;CAIA,MAAM,aAAiC;EACrC,OAAO,YAAqB,IAAI;CAClC;CAEA,MAAM,gBAAgB,UAAsC;EAC1D,OAAO,iBAA0B,MAAM,UAAU,yBAAyB;CAC5E;AACF"}
@@ -1,6 +1,6 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import "./__smrt-register__-DwNoP4GB.js";
3
- import { t as Message } from "./Message-5Xz9tauF.js";
2
+ import "./__smrt-register__-CaGLtiEU.js";
3
+ import { t as Message } from "./Message-DXpqot5f.js";
4
4
  import { field, smrt } from "@happyvertical/smrt-core";
5
5
  //#region src/models/Email.ts
6
6
  var Email_exports = /* @__PURE__ */ __exportAll({ Email: () => Email });
@@ -334,4 +334,4 @@ Email = __decorateClass([smrt({
334
334
  //#endregion
335
335
  export { Email_exports as n, Email as t };
336
336
 
337
- //# sourceMappingURL=Email-MRtj6D2s.js.map
337
+ //# sourceMappingURL=Email-CMUK3aWU.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Email-MRtj6D2s.js","names":[],"sources":["../../src/models/Email.ts"],"sourcesContent":["/**\n * Email model - Email message extending the Message STI base\n *\n * Retains all email-specific fields (RFC 822 compliance, folders, etc.)\n * while inheriting common message fields from Message.\n */\n\n// Register the package manifest before this module's @smrt() decorator\n// runs, whichever chunk the library build places it in (#3098).\nimport '../__smrt-register__.js';\nimport { field, smrt } from '@happyvertical/smrt-core';\nimport type {\n EmailOptions,\n MessageSendResult,\n SendEmailOptions,\n} from '../types';\nimport { Message } from './Message';\n\n@smrt({\n tableStrategy: 'sti',\n api: { include: ['list', 'get', 'create', 'update', 'delete'] },\n mcp: { include: ['list', 'get'] },\n cli: { skipApiCheck: true },\n // Message is read-only over REST; this provider type is writable there.\n widenParentExposure: { api: ['create', 'update', 'delete'] },\n})\nexport class Email extends Message { static __smrtPackage__ = \"@happyvertical/smrt-messages\";\n // RFC 822 fields\n @field({\n description:\n 'The email reference assigned by the sender for tracking replies.',\n })\n messageId = ''; // RFC 822 Message-ID header\n @field({\n description: 'The sender-assigned reference of the email being replied to.',\n })\n inReplyTo = '';\n\n // Additional recipients\n @field({ description: 'Recipients receiving a visible copy of this email.' })\n ccAddresses = '';\n @field({\n description:\n 'Recipients receiving a copy without appearing in the visible recipient list.',\n })\n bccAddresses = '';\n @field({ description: 'The address replies should be sent to.' })\n replyToAddress = '';\n @field({ description: 'The name associated with the reply address.' })\n replyToName = '';\n\n // Email-specific content\n @field({ description: 'The plain-text version of the email.' })\n textBody = '';\n @field({ description: 'The formatted version of the email.' })\n htmlBody = '';\n\n // Location\n @field({\n description:\n 'The mailbox folder reference supplied by the email connection.',\n })\n folderId = '';\n @field({\n description: 'The location of this email within the mailbox folders.',\n })\n folderPath = '';\n @field({\n description: 'Labels applied to this email by the mailbox provider.',\n })\n labels = ''; // JSON array (Gmail)\n @field({ description: 'Mailbox markers such as read, answered, or flagged.' })\n flags = ''; // JSON array (IMAP)\n\n // Email-specific status flags\n @field({ description: 'Whether this email has been replied to.' })\n isAnswered = false;\n @field({ description: 'Whether this email is saved as an unfinished draft.' })\n isDraft = false;\n\n // Raw data\n @field({\n description: 'The original email content as received from the mail server.',\n })\n rawMessage = '';\n @field({\n description: 'Delivery and routing details included with the email.',\n })\n headers = ''; // JSON object\n\n constructor(options: EmailOptions = {}) {\n super(options);\n\n if (options.messageId !== undefined) this.messageId = options.messageId;\n if (options.inReplyTo !== undefined) this.inReplyTo = options.inReplyTo;\n if (options.ccAddresses !== undefined)\n this.ccAddresses = options.ccAddresses;\n if (options.bccAddresses !== undefined)\n this.bccAddresses = options.bccAddresses;\n if (options.replyToAddress !== undefined)\n this.replyToAddress = options.replyToAddress;\n if (options.replyToName !== undefined)\n this.replyToName = options.replyToName;\n if (options.textBody !== undefined) this.textBody = options.textBody;\n if (options.htmlBody !== undefined) this.htmlBody = options.htmlBody;\n if (options.folderId !== undefined) this.folderId = options.folderId;\n if (options.folderPath !== undefined) this.folderPath = options.folderPath;\n if (options.labels !== undefined) this.labels = options.labels;\n if (options.flags !== undefined) this.flags = options.flags;\n if (options.isAnswered !== undefined) this.isAnswered = options.isAnswered;\n if (options.isDraft !== undefined) this.isDraft = options.isDraft;\n if (options.rawMessage !== undefined) this.rawMessage = options.rawMessage;\n if (options.headers !== undefined) this.headers = options.headers;\n\n // Populate base body from textBody for unified access\n if (options.textBody && !options.body) {\n this.body = options.textBody;\n }\n }\n\n /**\n * Get CC addresses as parsed array\n */\n getCcAddresses(): Array<{ address: string; name?: string }> {\n if (!this.ccAddresses) return [];\n try {\n return JSON.parse(this.ccAddresses);\n } catch {\n return [];\n }\n }\n\n /**\n * Get BCC addresses as parsed array\n */\n getBccAddresses(): Array<{ address: string; name?: string }> {\n if (!this.bccAddresses) return [];\n try {\n return JSON.parse(this.bccAddresses);\n } catch {\n return [];\n }\n }\n\n /**\n * Get labels as parsed array\n */\n getLabels(): string[] {\n if (!this.labels) return [];\n try {\n return JSON.parse(this.labels);\n } catch {\n return [];\n }\n }\n\n /**\n * Set labels from array\n */\n setLabels(labels: string[]): void {\n this.labels = JSON.stringify(labels);\n }\n\n /**\n * Get flags as parsed array\n */\n getFlags(): string[] {\n if (!this.flags) return [];\n try {\n return JSON.parse(this.flags);\n } catch {\n return [];\n }\n }\n\n /**\n * Set flags from array\n */\n setFlags(flags: string[]): void {\n this.flags = JSON.stringify(flags);\n }\n\n /**\n * Get headers as parsed object\n */\n getHeaders(): Record<string, string | string[]> {\n if (!this.headers) return {};\n try {\n return JSON.parse(this.headers);\n } catch {\n return {};\n }\n }\n\n /**\n * Set headers from object\n */\n setHeaders(headers: Record<string, string | string[]>): void {\n this.headers = JSON.stringify(headers);\n }\n\n /**\n * Get the email account (typed as EmailAccount)\n */\n override async getAccount() {\n if (!this.accountId) return null;\n\n const { EmailAccountCollection } = await import(\n '../collections/EmailAccountCollection'\n );\n const collection = await EmailAccountCollection.create(this.options);\n\n return await collection.get({ id: this.accountId });\n }\n\n /**\n * Get the folder\n */\n async getFolder() {\n if (!this.folderId) return null;\n\n const { EmailFolderCollection } = await import(\n '../collections/EmailFolderCollection'\n );\n const collection = await EmailFolderCollection.create(this.options);\n\n return await collection.get({ id: this.folderId });\n }\n\n /**\n * Get emails in the same thread\n */\n async getThreadEmails(): Promise<Email[]> {\n if (!this.threadId) return [this];\n\n const { EmailCollection } = await import('../collections/EmailCollection');\n const collection = await EmailCollection.create(this.options);\n\n return await collection.getByThread(this.threadId);\n }\n\n /**\n * Get a short preview of the email body\n */\n override getPreview(maxLength = 200): string {\n const body = this.textBody || this.htmlBody?.replace(/<[^>]*>/g, '') || '';\n if (body.length <= maxLength) return body;\n return `${body.slice(0, maxLength)}...`;\n }\n\n /**\n * Get References header values as array\n */\n getReferences(): string[] {\n const headers = this.getHeaders();\n const refs = headers.references;\n if (!refs) return [];\n if (Array.isArray(refs)) return refs;\n return refs.split(/\\s+/).filter(Boolean);\n }\n\n /**\n * Create a reply to this email with RFC 822 threading\n */\n override createReply(options?: { replyAll?: boolean }): Email {\n const reply = new Email({\n ...this.draftOptions(),\n id: undefined,\n accountId: this.accountId,\n threadId: this.threadId || this.id || undefined,\n subject: this.subject.startsWith('Re:')\n ? this.subject\n : `Re: ${this.subject}`,\n fromAddress: '',\n fromName: '',\n inReplyToMessageId: this.id || undefined,\n sendStatus: 'draft',\n isRead: true,\n isDraft: true,\n date: null,\n createdAt: undefined,\n updatedAt: undefined,\n\n // RFC 822 threading\n inReplyTo: this.messageId,\n\n // To: original sender\n toAddresses: JSON.stringify([\n { address: this.fromAddress, name: this.fromName },\n ]),\n });\n\n // RFC 822 References: original references + original Message-ID\n const refs = [...this.getReferences()];\n if (this.messageId && !refs.includes(this.messageId)) {\n refs.push(this.messageId);\n }\n reply.setHeaders({ references: refs.join(' ') });\n\n // Reply-All: add original To/CC as CC (excluding self)\n if (options?.replyAll) {\n const allRecipients = [\n ...this.getToAddresses(),\n ...this.getCcAddresses(),\n ];\n // Remove original sender (already in To:) and any duplicates\n const seen = new Set([this.fromAddress.toLowerCase()]);\n const ccAddresses = allRecipients.filter((r) => {\n const addr = r.address.toLowerCase();\n if (seen.has(addr)) return false;\n seen.add(addr);\n return true;\n });\n reply.ccAddresses = JSON.stringify(ccAddresses);\n }\n\n reply.body = this.buildQuotedBody();\n reply.textBody = reply.body;\n\n return reply;\n }\n\n /**\n * Create a forward of this email\n */\n override createForward(): Email {\n const forwardBody = this.buildForwardBody();\n\n const forward = new Email({\n ...this.draftOptions(),\n id: undefined,\n accountId: this.accountId,\n threadId: '',\n subject: this.subject.startsWith('Fwd:')\n ? this.subject\n : `Fwd: ${this.subject}`,\n toAddresses: '[]',\n ccAddresses: '[]',\n bccAddresses: '[]',\n fromAddress: '',\n fromName: '',\n body: forwardBody,\n textBody: forwardBody,\n hasAttachments: this.hasAttachments,\n inReplyToMessageId: '',\n inReplyTo: '',\n sendStatus: 'draft',\n isDraft: true,\n isRead: true,\n date: null,\n createdAt: undefined,\n updatedAt: undefined,\n });\n\n return forward;\n }\n\n /**\n * Build email-specific quoted body for replies\n */\n protected override buildQuotedBody(): string {\n const dateStr = this.date ? this.date.toLocaleString() : 'unknown date';\n const from = this.fromName\n ? `${this.fromName} <${this.fromAddress}>`\n : this.fromAddress;\n\n const bodyText = this.textBody || this.body || '';\n const quotedLines = bodyText\n .split('\\n')\n .map((line) => `> ${line}`)\n .join('\\n');\n\n return `\\n\\nOn ${dateStr}, ${from} wrote:\\n${quotedLines}`;\n }\n\n /**\n * Build forwarded message body\n */\n private buildForwardBody(): string {\n const dateStr = this.date ? this.date.toLocaleString() : 'unknown date';\n const from = this.fromName\n ? `${this.fromName} <${this.fromAddress}>`\n : this.fromAddress;\n\n const toStr = this.getToAddresses()\n .map((r) => (r.name ? `${r.name} <${r.address}>` : r.address))\n .join(', ');\n\n const bodyText = this.textBody || this.body || '';\n\n return [\n '',\n '',\n '---------- Forwarded message ----------',\n `From: ${from}`,\n `Date: ${dateStr}`,\n `Subject: ${this.subject}`,\n `To: ${toStr}`,\n '',\n bodyText,\n ].join('\\n');\n }\n\n /**\n * Send this email. Accepts {@link SendEmailOptions}, including send-time\n * `attachments`, which `EmailSender` merges with the stored rows (#3808).\n */\n override async send(options?: SendEmailOptions): Promise<MessageSendResult> {\n const result = await super.send(options);\n await this.markHasAttachments(options, result);\n return result;\n }\n\n /** Retry a failed send; takes the same options as {@link Email.send}. */\n override async retrySend(\n options?: SendEmailOptions,\n ): Promise<MessageSendResult> {\n const result = await super.retrySend(options);\n await this.markHasAttachments(options, result);\n return result;\n }\n\n /**\n * Record `hasAttachments` once send-time bytes were actually delivered, so a\n * rejected or failed send never claims attachments that did not go out.\n */\n private async markHasAttachments(\n options: SendEmailOptions | undefined,\n result: MessageSendResult,\n ): Promise<void> {\n if (!result.success || !options?.attachments?.length) return;\n if (this.hasAttachments) return;\n this.hasAttachments = true;\n if (this.isPersisted) await this.save();\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AA0BO,IAAM,QAAN,cAAoB,QAAQ;CAMjC,YAAY;CAIZ,YAAY;CAIZ,cAAc;CAKd,eAAe;CAEf,iBAAiB;CAEjB,cAAc;CAId,WAAW;CAEX,WAAW;CAOX,WAAW;CAIX,aAAa;CAIb,SAAS;CAET,QAAQ;CAIR,aAAa;CAEb,UAAU;CAMV,aAAa;CAIb,UAAU;CAEV,YAAY,UAAwB,CAAC,GAAG;EACtC,MAAM,OAAO;EAEb,IAAI,QAAQ,cAAc,KAAA,GAAW,KAAK,YAAY,QAAQ;EAC9D,IAAI,QAAQ,cAAc,KAAA,GAAW,KAAK,YAAY,QAAQ;EAC9D,IAAI,QAAQ,gBAAgB,KAAA,GAC1B,KAAK,cAAc,QAAQ;EAC7B,IAAI,QAAQ,iBAAiB,KAAA,GAC3B,KAAK,eAAe,QAAQ;EAC9B,IAAI,QAAQ,mBAAmB,KAAA,GAC7B,KAAK,iBAAiB,QAAQ;EAChC,IAAI,QAAQ,gBAAgB,KAAA,GAC1B,KAAK,cAAc,QAAQ;EAC7B,IAAI,QAAQ,aAAa,KAAA,GAAW,KAAK,WAAW,QAAQ;EAC5D,IAAI,QAAQ,aAAa,KAAA,GAAW,KAAK,WAAW,QAAQ;EAC5D,IAAI,QAAQ,aAAa,KAAA,GAAW,KAAK,WAAW,QAAQ;EAC5D,IAAI,QAAQ,eAAe,KAAA,GAAW,KAAK,aAAa,QAAQ;EAChE,IAAI,QAAQ,WAAW,KAAA,GAAW,KAAK,SAAS,QAAQ;EACxD,IAAI,QAAQ,UAAU,KAAA,GAAW,KAAK,QAAQ,QAAQ;EACtD,IAAI,QAAQ,eAAe,KAAA,GAAW,KAAK,aAAa,QAAQ;EAChE,IAAI,QAAQ,YAAY,KAAA,GAAW,KAAK,UAAU,QAAQ;EAC1D,IAAI,QAAQ,eAAe,KAAA,GAAW,KAAK,aAAa,QAAQ;EAChE,IAAI,QAAQ,YAAY,KAAA,GAAW,KAAK,UAAU,QAAQ;EAG1D,IAAI,QAAQ,YAAY,CAAC,QAAQ,MAC/B,KAAK,OAAO,QAAQ;CAExB;;;;CAKA,iBAA4D;EAC1D,IAAI,CAAC,KAAK,aAAa,OAAO,CAAC;EAC/B,IAAI;GACF,OAAO,KAAK,MAAM,KAAK,WAAW;EACpC,QAAQ;GACN,OAAO,CAAC;EACV;CACF;;;;CAKA,kBAA6D;EAC3D,IAAI,CAAC,KAAK,cAAc,OAAO,CAAC;EAChC,IAAI;GACF,OAAO,KAAK,MAAM,KAAK,YAAY;EACrC,QAAQ;GACN,OAAO,CAAC;EACV;CACF;;;;CAKA,YAAsB;EACpB,IAAI,CAAC,KAAK,QAAQ,OAAO,CAAC;EAC1B,IAAI;GACF,OAAO,KAAK,MAAM,KAAK,MAAM;EAC/B,QAAQ;GACN,OAAO,CAAC;EACV;CACF;;;;CAKA,UAAU,QAAwB;EAChC,KAAK,SAAS,KAAK,UAAU,MAAM;CACrC;;;;CAKA,WAAqB;EACnB,IAAI,CAAC,KAAK,OAAO,OAAO,CAAC;EACzB,IAAI;GACF,OAAO,KAAK,MAAM,KAAK,KAAK;EAC9B,QAAQ;GACN,OAAO,CAAC;EACV;CACF;;;;CAKA,SAAS,OAAuB;EAC9B,KAAK,QAAQ,KAAK,UAAU,KAAK;CACnC;;;;CAKA,aAAgD;EAC9C,IAAI,CAAC,KAAK,SAAS,OAAO,CAAC;EAC3B,IAAI;GACF,OAAO,KAAK,MAAM,KAAK,OAAO;EAChC,QAAQ;GACN,OAAO,CAAC;EACV;CACF;;;;CAKA,WAAW,SAAkD;EAC3D,KAAK,UAAU,KAAK,UAAU,OAAO;CACvC;;;;CAKA,MAAe,aAAa;EAC1B,IAAI,CAAC,KAAK,WAAW,OAAO;EAE5B,MAAM,EAAE,2BAA2B,MAAM,OACvC,cAAA,CAAA,MAAA,MAAA,EAAA,CAAA;EAIF,OAAO,OAAM,MAFY,uBAAuB,OAAO,KAAK,OAAO,EAAA,CAE3C,IAAI,EAAE,IAAI,KAAK,UAAU,CAAC;CACpD;;;;CAKA,MAAM,YAAY;EAChB,IAAI,CAAC,KAAK,UAAU,OAAO;EAE3B,MAAM,EAAE,0BAA0B,MAAM,OACtC,cAAA,CAAA,MAAA,MAAA,EAAA,CAAA;EAIF,OAAO,OAAM,MAFY,sBAAsB,OAAO,KAAK,OAAO,EAAA,CAE1C,IAAI,EAAE,IAAI,KAAK,SAAS,CAAC;CACnD;;;;CAKA,MAAM,kBAAoC;EACxC,IAAI,CAAC,KAAK,UAAU,OAAO,CAAC,IAAI;EAEhC,MAAM,EAAE,oBAAoB,MAAM,OAAO,cAAA,CAAA,MAAA,MAAA,EAAA,CAAA;EAGzC,OAAO,OAAM,MAFY,gBAAgB,OAAO,KAAK,OAAO,EAAA,CAEpC,YAAY,KAAK,QAAQ;CACnD;;;;CAKS,WAAW,YAAY,KAAa;EAC3C,MAAM,OAAO,KAAK,YAAY,KAAK,UAAU,QAAQ,YAAY,EAAE,KAAK;EACxE,IAAI,KAAK,UAAU,WAAW,OAAO;EACrC,OAAO,GAAG,KAAK,MAAM,GAAG,SAAS,EAAC;CACpC;;;;CAKA,gBAA0B;EAExB,MAAM,OADU,KAAK,WACR,CAAA,CAAQ;EACrB,IAAI,CAAC,MAAM,OAAO,CAAC;EACnB,IAAI,MAAM,QAAQ,IAAI,GAAG,OAAO;EAChC,OAAO,KAAK,MAAM,KAAK,CAAA,CAAE,OAAO,OAAO;CACzC;;;;CAKS,YAAY,SAAyC;EAC5D,MAAM,QAAQ,IAAI,MAAM;GACtB,GAAG,KAAK,aAAa;GACrB,IAAI,KAAA;GACJ,WAAW,KAAK;GAChB,UAAU,KAAK,YAAY,KAAK,MAAM,KAAA;GACtC,SAAS,KAAK,QAAQ,WAAW,KAAK,IAClC,KAAK,UACL,OAAO,KAAK;GAChB,aAAa;GACb,UAAU;GACV,oBAAoB,KAAK,MAAM,KAAA;GAC/B,YAAY;GACZ,QAAQ;GACR,SAAS;GACT,MAAM;GACN,WAAW,KAAA;GACX,WAAW,KAAA;GAGX,WAAW,KAAK;GAGhB,aAAa,KAAK,UAAU,CAC1B;IAAE,SAAS,KAAK;IAAa,MAAM,KAAK;GAAS,CACnD,CAAC;EACH,CAAC;EAGD,MAAM,OAAO,CAAC,GAAG,KAAK,cAAc,CAAC;EACrC,IAAI,KAAK,aAAa,CAAC,KAAK,SAAS,KAAK,SAAS,GACjD,KAAK,KAAK,KAAK,SAAS;EAE1B,MAAM,WAAW,EAAE,YAAY,KAAK,KAAK,GAAG,EAAE,CAAC;EAG/C,IAAI,SAAS,UAAU;GACrB,MAAM,gBAAgB,CACpB,GAAG,KAAK,eAAe,GACvB,GAAG,KAAK,eAAe,CACzB;GAEA,MAAM,uBAAO,IAAI,IAAI,CAAC,KAAK,YAAY,YAAY,CAAC,CAAC;GACrD,MAAM,cAAc,cAAc,QAAQ,MAAM;IAC9C,MAAM,OAAO,EAAE,QAAQ,YAAY;IACnC,IAAI,KAAK,IAAI,IAAI,GAAG,OAAO;IAC3B,KAAK,IAAI,IAAI;IACb,OAAO;GACT,CAAC;GACD,MAAM,cAAc,KAAK,UAAU,WAAW;EAChD;EAEA,MAAM,OAAO,KAAK,gBAAgB;EAClC,MAAM,WAAW,MAAM;EAEvB,OAAO;CACT;;;;CAKS,gBAAuB;EAC9B,MAAM,cAAc,KAAK,iBAAiB;EA4B1C,OAAO,IA1Ba,MAAM;GACxB,GAAG,KAAK,aAAa;GACrB,IAAI,KAAA;GACJ,WAAW,KAAK;GAChB,UAAU;GACV,SAAS,KAAK,QAAQ,WAAW,MAAM,IACnC,KAAK,UACL,QAAQ,KAAK;GACjB,aAAa;GACb,aAAa;GACb,cAAc;GACd,aAAa;GACb,UAAU;GACV,MAAM;GACN,UAAU;GACV,gBAAgB,KAAK;GACrB,oBAAoB;GACpB,WAAW;GACX,YAAY;GACZ,SAAS;GACT,QAAQ;GACR,MAAM;GACN,WAAW,KAAA;GACX,WAAW,KAAA;EACb,CAEO;CACT;;;;CAKmB,kBAA0B;EAY3C,OAAO;;KAXS,KAAK,OAAO,KAAK,KAAK,eAAe,IAAI,eAWjC,IAVX,KAAK,WACd,GAAG,KAAK,SAAQ,IAAK,KAAK,YAAW,KACrC,KAAK,YAQwB;GANhB,KAAK,YAAY,KAAK,QAAQ,GAAA,CAE5C,MAAM,IAAI,CAAA,CACV,KAAK,SAAS,KAAK,MAAM,CAAA,CACzB,KAAK,IAEqC;CAC/C;;;;CAKQ,mBAA2B;EACjC,MAAM,UAAU,KAAK,OAAO,KAAK,KAAK,eAAe,IAAI;EACzD,MAAM,OAAO,KAAK,WACd,GAAG,KAAK,SAAQ,IAAK,KAAK,YAAW,KACrC,KAAK;EAET,MAAM,QAAQ,KAAK,eAAe,CAAA,CAC/B,KAAK,MAAO,EAAE,OAAO,GAAG,EAAE,KAAI,IAAK,EAAE,QAAO,KAAM,EAAE,OAAQ,CAAA,CAC5D,KAAK,IAAI;EAEZ,MAAM,WAAW,KAAK,YAAY,KAAK,QAAQ;EAE/C,OAAO;GACL;GACA;GACA;GACA,SAAS;GACT,SAAS;GACT,YAAY,KAAK;GACjB,OAAO;GACP;GACA;EACF,CAAA,CAAE,KAAK,IAAI;CACb;;;;;CAMA,MAAe,KAAK,SAAwD;EAC1E,MAAM,SAAS,MAAM,MAAM,KAAK,OAAO;EACvC,MAAM,KAAK,mBAAmB,SAAS,MAAM;EAC7C,OAAO;CACT;;CAGA,MAAe,UACb,SAC4B;EAC5B,MAAM,SAAS,MAAM,MAAM,UAAU,OAAO;EAC5C,MAAM,KAAK,mBAAmB,SAAS,MAAM;EAC7C,OAAO;CACT;;;;;CAMA,MAAc,mBACZ,SACA,QACe;EACf,IAAI,CAAC,OAAO,WAAW,CAAC,SAAS,aAAa,QAAQ;EACtD,IAAI,KAAK,gBAAgB;EACzB,KAAK,iBAAiB;EACtB,IAAI,KAAK,aAAa,MAAM,KAAK,KAAK;CACxC;AACF;AAzZqC,cAAxB,OAA+B,mBAAkB,8BAAA;AAM5D,gBAAA,CAJC,MAAM,EACL,aACE,mEACJ,CAAC,CAAA,GALU,MAMX,WAAA,aAAA,CAAA;AAIA,gBAAA,CAHC,MAAM,EACL,aAAa,+DACf,CAAC,CAAA,GATU,MAUX,WAAA,aAAA,CAAA;AAIA,gBAAA,CADC,MAAM,EAAE,aAAa,qDAAqD,CAAC,CAAA,GAbjE,MAcX,WAAA,eAAA,CAAA;AAKA,gBAAA,CAJC,MAAM,EACL,aACE,+EACJ,CAAC,CAAA,GAlBU,MAmBX,WAAA,gBAAA,CAAA;AAEA,gBAAA,CADC,MAAM,EAAE,aAAa,yCAAyC,CAAC,CAAA,GApBrD,MAqBX,WAAA,kBAAA,CAAA;AAEA,gBAAA,CADC,MAAM,EAAE,aAAa,8CAA8C,CAAC,CAAA,GAtB1D,MAuBX,WAAA,eAAA,CAAA;AAIA,gBAAA,CADC,MAAM,EAAE,aAAa,uCAAuC,CAAC,CAAA,GA1BnD,MA2BX,WAAA,YAAA,CAAA;AAEA,gBAAA,CADC,MAAM,EAAE,aAAa,sCAAsC,CAAC,CAAA,GA5BlD,MA6BX,WAAA,YAAA,CAAA;AAOA,gBAAA,CAJC,MAAM,EACL,aACE,iEACJ,CAAC,CAAA,GAnCU,MAoCX,WAAA,YAAA,CAAA;AAIA,gBAAA,CAHC,MAAM,EACL,aAAa,yDACf,CAAC,CAAA,GAvCU,MAwCX,WAAA,cAAA,CAAA;AAIA,gBAAA,CAHC,MAAM,EACL,aAAa,wDACf,CAAC,CAAA,GA3CU,MA4CX,WAAA,UAAA,CAAA;AAEA,gBAAA,CADC,MAAM,EAAE,aAAa,sDAAsD,CAAC,CAAA,GA7ClE,MA8CX,WAAA,SAAA,CAAA;AAIA,gBAAA,CADC,MAAM,EAAE,aAAa,0CAA0C,CAAC,CAAA,GAjDtD,MAkDX,WAAA,cAAA,CAAA;AAEA,gBAAA,CADC,MAAM,EAAE,aAAa,sDAAsD,CAAC,CAAA,GAnDlE,MAoDX,WAAA,WAAA,CAAA;AAMA,gBAAA,CAHC,MAAM,EACL,aAAa,+DACf,CAAC,CAAA,GAzDU,MA0DX,WAAA,cAAA,CAAA;AAIA,gBAAA,CAHC,MAAM,EACL,aAAa,wDACf,CAAC,CAAA,GA7DU,MA8DX,WAAA,WAAA,CAAA;AA9DW,QAAN,gBAAA,CARN,KAAK;CACJ,eAAe;CACf,KAAK,EAAE,SAAS;EAAC;EAAQ;EAAO;EAAU;EAAU;CAAQ,EAAE;CAC9D,KAAK,EAAE,SAAS,CAAC,QAAQ,KAAK,EAAE;CAChC,KAAK,EAAE,cAAc,KAAK;CAE1B,qBAAqB,EAAE,KAAK;EAAC;EAAU;EAAU;CAAQ,EAAE;AAC7D,CAAC,CAAA,GACY,KAAA"}
1
+ {"version":3,"file":"Email-CMUK3aWU.js","names":[],"sources":["../../src/models/Email.ts"],"sourcesContent":["/**\n * Email model - Email message extending the Message STI base\n *\n * Retains all email-specific fields (RFC 822 compliance, folders, etc.)\n * while inheriting common message fields from Message.\n */\n\n// Register the package manifest before this module's @smrt() decorator\n// runs, whichever chunk the library build places it in (#3098).\nimport '../__smrt-register__.js';\nimport { field, smrt } from '@happyvertical/smrt-core';\nimport type {\n EmailOptions,\n MessageSendResult,\n SendEmailOptions,\n} from '../types';\nimport { Message } from './Message';\n\n@smrt({\n tableStrategy: 'sti',\n api: { include: ['list', 'get', 'create', 'update', 'delete'] },\n mcp: { include: ['list', 'get'] },\n cli: { skipApiCheck: true },\n // Message is read-only over REST; this provider type is writable there.\n widenParentExposure: { api: ['create', 'update', 'delete'] },\n})\nexport class Email extends Message { static __smrtPackage__ = \"@happyvertical/smrt-messages\";\n // RFC 822 fields\n @field({\n description:\n 'The email reference assigned by the sender for tracking replies.',\n })\n messageId = ''; // RFC 822 Message-ID header\n @field({\n description: 'The sender-assigned reference of the email being replied to.',\n })\n inReplyTo = '';\n\n // Additional recipients\n @field({ description: 'Recipients receiving a visible copy of this email.' })\n ccAddresses = '';\n @field({\n description:\n 'Recipients receiving a copy without appearing in the visible recipient list.',\n })\n bccAddresses = '';\n @field({ description: 'The address replies should be sent to.' })\n replyToAddress = '';\n @field({ description: 'The name associated with the reply address.' })\n replyToName = '';\n\n // Email-specific content\n @field({ description: 'The plain-text version of the email.' })\n textBody = '';\n @field({ description: 'The formatted version of the email.' })\n htmlBody = '';\n\n // Location\n @field({\n description:\n 'The mailbox folder reference supplied by the email connection.',\n })\n folderId = '';\n @field({\n description: 'The location of this email within the mailbox folders.',\n })\n folderPath = '';\n @field({\n description: 'Labels applied to this email by the mailbox provider.',\n })\n labels = ''; // JSON array (Gmail)\n @field({ description: 'Mailbox markers such as read, answered, or flagged.' })\n flags = ''; // JSON array (IMAP)\n\n // Email-specific status flags\n @field({ description: 'Whether this email has been replied to.' })\n isAnswered = false;\n @field({ description: 'Whether this email is saved as an unfinished draft.' })\n isDraft = false;\n\n // Raw data\n @field({\n description: 'The original email content as received from the mail server.',\n })\n rawMessage = '';\n @field({\n description: 'Delivery and routing details included with the email.',\n })\n headers = ''; // JSON object\n\n constructor(options: EmailOptions = {}) {\n super(options);\n\n if (options.messageId !== undefined) this.messageId = options.messageId;\n if (options.inReplyTo !== undefined) this.inReplyTo = options.inReplyTo;\n if (options.ccAddresses !== undefined)\n this.ccAddresses = options.ccAddresses;\n if (options.bccAddresses !== undefined)\n this.bccAddresses = options.bccAddresses;\n if (options.replyToAddress !== undefined)\n this.replyToAddress = options.replyToAddress;\n if (options.replyToName !== undefined)\n this.replyToName = options.replyToName;\n if (options.textBody !== undefined) this.textBody = options.textBody;\n if (options.htmlBody !== undefined) this.htmlBody = options.htmlBody;\n if (options.folderId !== undefined) this.folderId = options.folderId;\n if (options.folderPath !== undefined) this.folderPath = options.folderPath;\n if (options.labels !== undefined) this.labels = options.labels;\n if (options.flags !== undefined) this.flags = options.flags;\n if (options.isAnswered !== undefined) this.isAnswered = options.isAnswered;\n if (options.isDraft !== undefined) this.isDraft = options.isDraft;\n if (options.rawMessage !== undefined) this.rawMessage = options.rawMessage;\n if (options.headers !== undefined) this.headers = options.headers;\n\n // Populate base body from textBody for unified access\n if (options.textBody && !options.body) {\n this.body = options.textBody;\n }\n }\n\n /**\n * Get CC addresses as parsed array\n */\n getCcAddresses(): Array<{ address: string; name?: string }> {\n if (!this.ccAddresses) return [];\n try {\n return JSON.parse(this.ccAddresses);\n } catch {\n return [];\n }\n }\n\n /**\n * Get BCC addresses as parsed array\n */\n getBccAddresses(): Array<{ address: string; name?: string }> {\n if (!this.bccAddresses) return [];\n try {\n return JSON.parse(this.bccAddresses);\n } catch {\n return [];\n }\n }\n\n /**\n * Get labels as parsed array\n */\n getLabels(): string[] {\n if (!this.labels) return [];\n try {\n return JSON.parse(this.labels);\n } catch {\n return [];\n }\n }\n\n /**\n * Set labels from array\n */\n setLabels(labels: string[]): void {\n this.labels = JSON.stringify(labels);\n }\n\n /**\n * Get flags as parsed array\n */\n getFlags(): string[] {\n if (!this.flags) return [];\n try {\n return JSON.parse(this.flags);\n } catch {\n return [];\n }\n }\n\n /**\n * Set flags from array\n */\n setFlags(flags: string[]): void {\n this.flags = JSON.stringify(flags);\n }\n\n /**\n * Get headers as parsed object\n */\n getHeaders(): Record<string, string | string[]> {\n if (!this.headers) return {};\n try {\n return JSON.parse(this.headers);\n } catch {\n return {};\n }\n }\n\n /**\n * Set headers from object\n */\n setHeaders(headers: Record<string, string | string[]>): void {\n this.headers = JSON.stringify(headers);\n }\n\n /**\n * Get the email account (typed as EmailAccount)\n */\n override async getAccount() {\n if (!this.accountId) return null;\n\n const { EmailAccountCollection } = await import(\n '../collections/EmailAccountCollection'\n );\n const collection = await EmailAccountCollection.create(this.options);\n\n return await collection.get({ id: this.accountId });\n }\n\n /**\n * Get the folder\n */\n async getFolder() {\n if (!this.folderId) return null;\n\n const { EmailFolderCollection } = await import(\n '../collections/EmailFolderCollection'\n );\n const collection = await EmailFolderCollection.create(this.options);\n\n return await collection.get({ id: this.folderId });\n }\n\n /**\n * Get emails in the same thread\n */\n async getThreadEmails(): Promise<Email[]> {\n if (!this.threadId) return [this];\n\n const { EmailCollection } = await import('../collections/EmailCollection');\n const collection = await EmailCollection.create(this.options);\n\n return await collection.getByThread(this.threadId);\n }\n\n /**\n * Get a short preview of the email body\n */\n override getPreview(maxLength = 200): string {\n const body = this.textBody || this.htmlBody?.replace(/<[^>]*>/g, '') || '';\n if (body.length <= maxLength) return body;\n return `${body.slice(0, maxLength)}...`;\n }\n\n /**\n * Get References header values as array\n */\n getReferences(): string[] {\n const headers = this.getHeaders();\n const refs = headers.references;\n if (!refs) return [];\n if (Array.isArray(refs)) return refs;\n return refs.split(/\\s+/).filter(Boolean);\n }\n\n /**\n * Create a reply to this email with RFC 822 threading\n */\n override createReply(options?: { replyAll?: boolean }): Email {\n const reply = new Email({\n ...this.draftOptions(),\n id: undefined,\n accountId: this.accountId,\n threadId: this.threadId || this.id || undefined,\n subject: this.subject.startsWith('Re:')\n ? this.subject\n : `Re: ${this.subject}`,\n fromAddress: '',\n fromName: '',\n inReplyToMessageId: this.id || undefined,\n sendStatus: 'draft',\n isRead: true,\n isDraft: true,\n date: null,\n createdAt: undefined,\n updatedAt: undefined,\n\n // RFC 822 threading\n inReplyTo: this.messageId,\n\n // To: original sender\n toAddresses: JSON.stringify([\n { address: this.fromAddress, name: this.fromName },\n ]),\n });\n\n // RFC 822 References: original references + original Message-ID\n const refs = [...this.getReferences()];\n if (this.messageId && !refs.includes(this.messageId)) {\n refs.push(this.messageId);\n }\n reply.setHeaders({ references: refs.join(' ') });\n\n // Reply-All: add original To/CC as CC (excluding self)\n if (options?.replyAll) {\n const allRecipients = [\n ...this.getToAddresses(),\n ...this.getCcAddresses(),\n ];\n // Remove original sender (already in To:) and any duplicates\n const seen = new Set([this.fromAddress.toLowerCase()]);\n const ccAddresses = allRecipients.filter((r) => {\n const addr = r.address.toLowerCase();\n if (seen.has(addr)) return false;\n seen.add(addr);\n return true;\n });\n reply.ccAddresses = JSON.stringify(ccAddresses);\n }\n\n reply.body = this.buildQuotedBody();\n reply.textBody = reply.body;\n\n return reply;\n }\n\n /**\n * Create a forward of this email\n */\n override createForward(): Email {\n const forwardBody = this.buildForwardBody();\n\n const forward = new Email({\n ...this.draftOptions(),\n id: undefined,\n accountId: this.accountId,\n threadId: '',\n subject: this.subject.startsWith('Fwd:')\n ? this.subject\n : `Fwd: ${this.subject}`,\n toAddresses: '[]',\n ccAddresses: '[]',\n bccAddresses: '[]',\n fromAddress: '',\n fromName: '',\n body: forwardBody,\n textBody: forwardBody,\n hasAttachments: this.hasAttachments,\n inReplyToMessageId: '',\n inReplyTo: '',\n sendStatus: 'draft',\n isDraft: true,\n isRead: true,\n date: null,\n createdAt: undefined,\n updatedAt: undefined,\n });\n\n return forward;\n }\n\n /**\n * Build email-specific quoted body for replies\n */\n protected override buildQuotedBody(): string {\n const dateStr = this.date ? this.date.toLocaleString() : 'unknown date';\n const from = this.fromName\n ? `${this.fromName} <${this.fromAddress}>`\n : this.fromAddress;\n\n const bodyText = this.textBody || this.body || '';\n const quotedLines = bodyText\n .split('\\n')\n .map((line) => `> ${line}`)\n .join('\\n');\n\n return `\\n\\nOn ${dateStr}, ${from} wrote:\\n${quotedLines}`;\n }\n\n /**\n * Build forwarded message body\n */\n private buildForwardBody(): string {\n const dateStr = this.date ? this.date.toLocaleString() : 'unknown date';\n const from = this.fromName\n ? `${this.fromName} <${this.fromAddress}>`\n : this.fromAddress;\n\n const toStr = this.getToAddresses()\n .map((r) => (r.name ? `${r.name} <${r.address}>` : r.address))\n .join(', ');\n\n const bodyText = this.textBody || this.body || '';\n\n return [\n '',\n '',\n '---------- Forwarded message ----------',\n `From: ${from}`,\n `Date: ${dateStr}`,\n `Subject: ${this.subject}`,\n `To: ${toStr}`,\n '',\n bodyText,\n ].join('\\n');\n }\n\n /**\n * Send this email. Accepts {@link SendEmailOptions}, including send-time\n * `attachments`, which `EmailSender` merges with the stored rows (#3808).\n */\n override async send(options?: SendEmailOptions): Promise<MessageSendResult> {\n const result = await super.send(options);\n await this.markHasAttachments(options, result);\n return result;\n }\n\n /** Retry a failed send; takes the same options as {@link Email.send}. */\n override async retrySend(\n options?: SendEmailOptions,\n ): Promise<MessageSendResult> {\n const result = await super.retrySend(options);\n await this.markHasAttachments(options, result);\n return result;\n }\n\n /**\n * Record `hasAttachments` once send-time bytes were actually delivered, so a\n * rejected or failed send never claims attachments that did not go out.\n */\n private async markHasAttachments(\n options: SendEmailOptions | undefined,\n result: MessageSendResult,\n ): Promise<void> {\n if (!result.success || !options?.attachments?.length) return;\n if (this.hasAttachments) return;\n this.hasAttachments = true;\n if (this.isPersisted) await this.save();\n }\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import "./__smrt-register__-DwNoP4GB.js";
2
+ import "./__smrt-register__-CaGLtiEU.js";
3
3
  import { SmrtObject, foreignKey, smrt } from "@happyvertical/smrt-core";
4
4
  import { TenantScoped, tenantId } from "@happyvertical/smrt-tenancy";
5
5
  //#region src/models/EmailFolder.ts
@@ -159,4 +159,4 @@ EmailFolder = __decorateClass([TenantScoped({ mode: "optional" }), smrt({
159
159
  //#endregion
160
160
  export { EmailFolder_exports as n, EmailFolder as t };
161
161
 
162
- //# sourceMappingURL=EmailFolder-qpOwfo_O.js.map
162
+ //# sourceMappingURL=EmailFolder-DLJvqNqI.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"EmailFolder-qpOwfo_O.js","names":[],"sources":["../../src/models/EmailFolder.ts"],"sourcesContent":["/**\n * EmailFolder model - Folder/label tracking\n */\n\n// Register the package manifest before this module's @smrt() decorator\n// runs, whichever chunk the library build places it in (#3098).\nimport '../__smrt-register__.js';\nimport { foreignKey, SmrtObject, smrt } from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport type { EmailFolderOptions } from '../types';\n\n@TenantScoped({ mode: 'optional' })\n@smrt({\n api: { include: ['list', 'get', 'create', 'update', 'delete'] },\n mcp: { include: ['list', 'get'] },\n cli: { skipApiCheck: true },\n})\nexport class EmailFolder extends SmrtObject { static __smrtPackage__ = \"@happyvertical/smrt-messages\";\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n @foreignKey('Account')\n accountId = '';\n name = '';\n path = '';\n delimiter = '/';\n specialUse = ''; // '\\\\Inbox', '\\\\Sent', '\\\\Drafts', etc.\n messageCount = 0;\n unreadCount = 0;\n subscribed = true;\n\n // Timestamps\n createdAt = new Date();\n updatedAt = new Date();\n\n constructor(options: EmailFolderOptions = {}) {\n super(options);\n\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.accountId !== undefined) this.accountId = options.accountId;\n if (options.name !== undefined) this.name = options.name;\n if (options.path !== undefined) this.path = options.path;\n if (options.delimiter !== undefined) this.delimiter = options.delimiter;\n if (options.specialUse !== undefined) this.specialUse = options.specialUse;\n if (options.messageCount !== undefined)\n this.messageCount = options.messageCount;\n if (options.unreadCount !== undefined)\n this.unreadCount = options.unreadCount;\n if (options.subscribed !== undefined) this.subscribed = options.subscribed;\n if (options.createdAt) this.createdAt = options.createdAt;\n if (options.updatedAt) this.updatedAt = options.updatedAt;\n }\n\n /**\n * Get the email account\n */\n async getAccount() {\n if (!this.accountId) return null;\n\n const { EmailAccountCollection } = await import(\n '../collections/EmailAccountCollection'\n );\n const collection = await EmailAccountCollection.create(this.options);\n\n return await collection.get({ id: this.accountId });\n }\n\n /**\n * Get all emails in this folder\n */\n async getEmails(limit?: number) {\n const { EmailCollection } = await import('../collections/EmailCollection');\n const collection = await EmailCollection.create(this.options);\n\n const options: { where: Record<string, unknown>; limit?: number } = {\n where: { folderId: this.id },\n };\n if (limit) {\n options.limit = limit;\n }\n\n return await collection.list(options);\n }\n\n /**\n * Get unread emails in this folder\n */\n async getUnreadEmails(limit?: number) {\n const { EmailCollection } = await import('../collections/EmailCollection');\n const collection = await EmailCollection.create(this.options);\n\n const options: { where: Record<string, unknown>; limit?: number } = {\n where: { folderId: this.id, isRead: false },\n };\n if (limit) {\n options.limit = limit;\n }\n\n return await collection.list(options);\n }\n\n /**\n * Update message counts from database\n */\n async refreshCounts(): Promise<void> {\n if (!this.id) {\n throw new Error(\n 'EmailFolder.refreshCounts requires a persisted folder (missing id)',\n );\n }\n const folderId = this.id;\n const { EmailCollection } = await import('../collections/EmailCollection');\n const collection = await EmailCollection.create(this.options);\n\n this.messageCount = await collection.countByFolder(folderId);\n this.unreadCount = await collection.countUnreadByFolder(folderId);\n this.updatedAt = new Date();\n await this.save();\n }\n\n /**\n * Check if this is the inbox folder\n */\n isInbox(): boolean {\n return this.specialUse === '\\\\Inbox' || this.path.toLowerCase() === 'inbox';\n }\n\n /**\n * Check if this is the sent folder\n */\n isSent(): boolean {\n return this.specialUse === '\\\\Sent' || this.path.toLowerCase() === 'sent';\n }\n\n /**\n * Check if this is the drafts folder\n */\n isDrafts(): boolean {\n return (\n this.specialUse === '\\\\Drafts' || this.path.toLowerCase() === 'drafts'\n );\n }\n\n /**\n * Check if this is the trash folder\n */\n isTrash(): boolean {\n return this.specialUse === '\\\\Trash' || this.path.toLowerCase() === 'trash';\n }\n\n /**\n * Check if this is the spam/junk folder\n */\n isSpam(): boolean {\n return (\n this.specialUse === '\\\\Junk' ||\n this.path.toLowerCase() === 'spam' ||\n this.path.toLowerCase() === 'junk'\n );\n }\n\n /**\n * Check if this is a system folder\n */\n isSystemFolder(): boolean {\n return !!this.specialUse;\n }\n\n /**\n * Subscribe to folder\n */\n async subscribe(): Promise<void> {\n this.subscribed = true;\n this.updatedAt = new Date();\n await this.save();\n }\n\n /**\n * Unsubscribe from folder\n */\n async unsubscribe(): Promise<void> {\n this.subscribed = false;\n this.updatedAt = new Date();\n await this.save();\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"EmailFolder-DLJvqNqI.js","names":[],"sources":["../../src/models/EmailFolder.ts"],"sourcesContent":["/**\n * EmailFolder model - Folder/label tracking\n */\n\n// Register the package manifest before this module's @smrt() decorator\n// runs, whichever chunk the library build places it in (#3098).\nimport '../__smrt-register__.js';\nimport { foreignKey, SmrtObject, smrt } from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport type { EmailFolderOptions } from '../types';\n\n@TenantScoped({ mode: 'optional' })\n@smrt({\n api: { include: ['list', 'get', 'create', 'update', 'delete'] },\n mcp: { include: ['list', 'get'] },\n cli: { skipApiCheck: true },\n})\nexport class EmailFolder extends SmrtObject { static __smrtPackage__ = \"@happyvertical/smrt-messages\";\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n @foreignKey('Account')\n accountId = '';\n name = '';\n path = '';\n delimiter = '/';\n specialUse = ''; // '\\\\Inbox', '\\\\Sent', '\\\\Drafts', etc.\n messageCount = 0;\n unreadCount = 0;\n subscribed = true;\n\n // Timestamps\n createdAt = new Date();\n updatedAt = new Date();\n\n constructor(options: EmailFolderOptions = {}) {\n super(options);\n\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.accountId !== undefined) this.accountId = options.accountId;\n if (options.name !== undefined) this.name = options.name;\n if (options.path !== undefined) this.path = options.path;\n if (options.delimiter !== undefined) this.delimiter = options.delimiter;\n if (options.specialUse !== undefined) this.specialUse = options.specialUse;\n if (options.messageCount !== undefined)\n this.messageCount = options.messageCount;\n if (options.unreadCount !== undefined)\n this.unreadCount = options.unreadCount;\n if (options.subscribed !== undefined) this.subscribed = options.subscribed;\n if (options.createdAt) this.createdAt = options.createdAt;\n if (options.updatedAt) this.updatedAt = options.updatedAt;\n }\n\n /**\n * Get the email account\n */\n async getAccount() {\n if (!this.accountId) return null;\n\n const { EmailAccountCollection } = await import(\n '../collections/EmailAccountCollection'\n );\n const collection = await EmailAccountCollection.create(this.options);\n\n return await collection.get({ id: this.accountId });\n }\n\n /**\n * Get all emails in this folder\n */\n async getEmails(limit?: number) {\n const { EmailCollection } = await import('../collections/EmailCollection');\n const collection = await EmailCollection.create(this.options);\n\n const options: { where: Record<string, unknown>; limit?: number } = {\n where: { folderId: this.id },\n };\n if (limit) {\n options.limit = limit;\n }\n\n return await collection.list(options);\n }\n\n /**\n * Get unread emails in this folder\n */\n async getUnreadEmails(limit?: number) {\n const { EmailCollection } = await import('../collections/EmailCollection');\n const collection = await EmailCollection.create(this.options);\n\n const options: { where: Record<string, unknown>; limit?: number } = {\n where: { folderId: this.id, isRead: false },\n };\n if (limit) {\n options.limit = limit;\n }\n\n return await collection.list(options);\n }\n\n /**\n * Update message counts from database\n */\n async refreshCounts(): Promise<void> {\n if (!this.id) {\n throw new Error(\n 'EmailFolder.refreshCounts requires a persisted folder (missing id)',\n );\n }\n const folderId = this.id;\n const { EmailCollection } = await import('../collections/EmailCollection');\n const collection = await EmailCollection.create(this.options);\n\n this.messageCount = await collection.countByFolder(folderId);\n this.unreadCount = await collection.countUnreadByFolder(folderId);\n this.updatedAt = new Date();\n await this.save();\n }\n\n /**\n * Check if this is the inbox folder\n */\n isInbox(): boolean {\n return this.specialUse === '\\\\Inbox' || this.path.toLowerCase() === 'inbox';\n }\n\n /**\n * Check if this is the sent folder\n */\n isSent(): boolean {\n return this.specialUse === '\\\\Sent' || this.path.toLowerCase() === 'sent';\n }\n\n /**\n * Check if this is the drafts folder\n */\n isDrafts(): boolean {\n return (\n this.specialUse === '\\\\Drafts' || this.path.toLowerCase() === 'drafts'\n );\n }\n\n /**\n * Check if this is the trash folder\n */\n isTrash(): boolean {\n return this.specialUse === '\\\\Trash' || this.path.toLowerCase() === 'trash';\n }\n\n /**\n * Check if this is the spam/junk folder\n */\n isSpam(): boolean {\n return (\n this.specialUse === '\\\\Junk' ||\n this.path.toLowerCase() === 'spam' ||\n this.path.toLowerCase() === 'junk'\n );\n }\n\n /**\n * Check if this is a system folder\n */\n isSystemFolder(): boolean {\n return !!this.specialUse;\n }\n\n /**\n * Subscribe to folder\n */\n async subscribe(): Promise<void> {\n this.subscribed = true;\n this.updatedAt = new Date();\n await this.save();\n }\n\n /**\n * Unsubscribe from folder\n */\n async unsubscribe(): Promise<void> {\n this.subscribed = false;\n this.updatedAt = new Date();\n await this.save();\n }\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import "./__smrt-register__-DwNoP4GB.js";
1
+ import "./__smrt-register__-CaGLtiEU.js";
2
2
  import { SmrtObject, crossPackageRef, field, foreignKey, smrt, usesEmbeddedRevisionFallback, withEmbeddedWriteQueue } from "@happyvertical/smrt-core";
3
3
  import { TenantScoped, tenantId } from "@happyvertical/smrt-tenancy";
4
4
  //#region src/models/Message.ts
@@ -17,6 +17,21 @@ var __decorateClass = (decorators, target, key, kind) => {
17
17
  return result;
18
18
  };
19
19
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
20
+ var SEND_OUTCOME_UNKNOWN_METADATA_KEY = "sendOutcomeUnknown";
21
+ function withSendOutcomeMarker(stored, outcomeUnknown) {
22
+ let bag = {};
23
+ if (typeof stored === "string" && stored.trim()) try {
24
+ const parsed = JSON.parse(stored);
25
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return;
26
+ bag = parsed;
27
+ } catch {
28
+ return;
29
+ }
30
+ if (Boolean(bag["sendOutcomeUnknown"]) === outcomeUnknown) return;
31
+ if (outcomeUnknown) bag[SEND_OUTCOME_UNKNOWN_METADATA_KEY] = true;
32
+ else delete bag[SEND_OUTCOME_UNKNOWN_METADATA_KEY];
33
+ return JSON.stringify(bag);
34
+ }
20
35
  var Message = class extends SmrtObject {
21
36
  tenantId = null;
22
37
  accountId = "";
@@ -87,11 +102,13 @@ var Message = class extends SmrtObject {
87
102
  const currentRevisionMs = new Date(String(currentRevision)).getTime();
88
103
  if (!Number.isFinite(currentRevisionMs)) return;
89
104
  nextRevision = new Date(Math.max(Date.now(), currentRevisionMs + 1));
105
+ const metadata = withSendOutcomeMarker(current.metadata, !result.success && result.outcomeUnknown === true);
90
106
  const values = {
91
107
  send_status: sendStatus,
92
108
  sent_at: result.success ? result.sentAt : null,
93
109
  send_error: sendError,
94
- updated_at: nextRevision
110
+ updated_at: nextRevision,
111
+ ...metadata === void 0 ? {} : { metadata }
95
112
  };
96
113
  if (useEmbeddedFallback) {
97
114
  await this.db.upsert(this.tableName, ["id"], {
@@ -191,7 +208,7 @@ var Message = class extends SmrtObject {
191
208
  */
192
209
  async getAccount() {
193
210
  if (!this.accountId) return null;
194
- const { AccountCollection } = await import("./AccountCollection-O-zbituV.js").then((n) => n.n);
211
+ const { AccountCollection } = await import("./AccountCollection-By48bdwt.js").then((n) => n.n);
195
212
  return await (await AccountCollection.create(this.options)).get({ id: this.accountId });
196
213
  }
197
214
  async getEndpoint() {
@@ -204,7 +221,7 @@ var Message = class extends SmrtObject {
204
221
  */
205
222
  async getThreadMessages() {
206
223
  if (!this.threadId) return [this];
207
- const { MessageCollection } = await import("./MessageCollection-m1C6fDwS.js").then((n) => n.n);
224
+ const { MessageCollection } = await import("./MessageCollection-CMGaSGBc.js").then((n) => n.n);
208
225
  return await (await MessageCollection.create(this.options)).list({ where: { threadId: this.threadId } });
209
226
  }
210
227
  /**
@@ -317,6 +334,7 @@ var Message = class extends SmrtObject {
317
334
  result = {
318
335
  success: false,
319
336
  error: error instanceof Error ? error.message : String(error),
337
+ outcomeUnknown: true,
320
338
  sentAt: /* @__PURE__ */ new Date()
321
339
  };
322
340
  }
@@ -324,6 +342,35 @@ var Message = class extends SmrtObject {
324
342
  return result;
325
343
  }
326
344
  /**
345
+ * Whether the latest failed send may still have been accepted by the
346
+ * provider (see `MessageSendResult.outcomeUnknown`).
347
+ */
348
+ isSendOutcomeUnknown() {
349
+ return this.sendStatus === "failed" && this.getMetadata()["sendOutcomeUnknown"] === true;
350
+ }
351
+ /**
352
+ * Release a send claim whose sender is gone (a crash or lost lease left the
353
+ * row in `sending`): move it to `failed` with an unknown outcome so the
354
+ * owner of the delivery decision can send it again. Returns false, changing
355
+ * nothing, unless the stored row is still `sending`. Only for callers that
356
+ * have established that no live sender holds the claim.
357
+ */
358
+ async abandonSendAttempt(reason) {
359
+ if (!this.id) return false;
360
+ if ((await this.getCanonicalPersistedRow({ id: this.id }))?.send_status !== "sending") return false;
361
+ try {
362
+ await this.finalizeSendLifecycle({
363
+ success: false,
364
+ error: reason,
365
+ outcomeUnknown: true,
366
+ sentAt: /* @__PURE__ */ new Date()
367
+ });
368
+ } catch {
369
+ return false;
370
+ }
371
+ return this.sendStatus === "failed";
372
+ }
373
+ /**
327
374
  * Retry sending a failed message
328
375
  */
329
376
  async retrySend(options) {
@@ -453,4 +500,4 @@ Message = __decorateClass([TenantScoped({ mode: "optional" }), smrt({
453
500
  //#endregion
454
501
  export { Message as t };
455
502
 
456
- //# sourceMappingURL=Message-5Xz9tauF.js.map
503
+ //# sourceMappingURL=Message-DXpqot5f.js.map