@cosmicdrift/kumiko-samples 0.392.2 → 0.394.0

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 (186) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +15 -5
  3. package/packages/bundled-features/src/account-security/README.md +1 -1
  4. package/packages/bundled-features/src/account-security/changes.json +6 -0
  5. package/packages/bundled-features/src/account-security/feature.ts +9 -1
  6. package/packages/bundled-features/src/audit/event-classification.ts +1 -0
  7. package/packages/bundled-features/src/auth-email-password/changes.json +6 -0
  8. package/packages/bundled-features/src/auth-email-password/web/auth-client.ts +10 -52
  9. package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +4 -4
  10. package/packages/bundled-features/src/auth-email-password/web/auth-redirect.ts +3 -12
  11. package/packages/bundled-features/src/auth-email-password/web/index.ts +2 -0
  12. package/packages/bundled-features/src/auth-email-password/web/login-failure.ts +54 -0
  13. package/packages/bundled-features/src/auth-email-password/web/login-url.ts +19 -0
  14. package/packages/bundled-features/src/auth-email-password/web/static-login.ts +183 -0
  15. package/packages/bundled-features/src/auth-mfa/web/mfa-client.ts +7 -63
  16. package/packages/bundled-features/src/billing-foundation/changes.json +30 -0
  17. package/packages/bundled-features/src/billing-foundation/checkout-core.ts +8 -16
  18. package/packages/bundled-features/src/billing-foundation/events.ts +2 -0
  19. package/packages/bundled-features/src/billing-foundation/feature.ts +1 -1
  20. package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +9 -2
  21. package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts +2 -0
  22. package/packages/bundled-features/src/billing-foundation/index.ts +2 -0
  23. package/packages/bundled-features/src/billing-foundation/one-off-checkout-gate.ts +44 -0
  24. package/packages/bundled-features/src/billing-foundation/one-off-customer.ts +17 -0
  25. package/packages/bundled-features/src/billing-foundation/plan-catalog.ts +1 -0
  26. package/packages/bundled-features/src/billing-foundation/types.ts +26 -0
  27. package/packages/bundled-features/src/billing-foundation/web/billing-plans-panel.tsx +1 -0
  28. package/packages/bundled-features/src/billing-foundation/web/plan-switch-confirm-dialog.tsx +8 -2
  29. package/packages/bundled-features/src/billing-foundation/web/plan-switch-recurring.ts +23 -0
  30. package/packages/bundled-features/src/billing-foundation/webhook-handler.ts +1 -0
  31. package/packages/bundled-features/src/cap-overview/changes.json +6 -0
  32. package/packages/bundled-features/src/cap-overview/handlers/caps-usage.query.ts +3 -3
  33. package/packages/bundled-features/src/cap-overview/types.ts +2 -0
  34. package/packages/bundled-features/src/compliance-profiles/web/compliance-profile-catalog.tsx +10 -1
  35. package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +12 -3
  36. package/packages/bundled-features/src/files/changes.json +18 -0
  37. package/packages/bundled-features/src/inbound-mail-foundation/account-health.ts +146 -0
  38. package/packages/bundled-features/src/inbound-mail-foundation/account-poll-schedule.ts +74 -0
  39. package/packages/bundled-features/src/inbound-mail-foundation/account-poll.ts +238 -0
  40. package/packages/bundled-features/src/inbound-mail-foundation/account-record.ts +2 -0
  41. package/packages/bundled-features/src/inbound-mail-foundation/account-wake.ts +21 -0
  42. package/packages/bundled-features/src/inbound-mail-foundation/changes.json +155 -0
  43. package/packages/bundled-features/src/inbound-mail-foundation/connect-callback-verify.ts +65 -0
  44. package/packages/bundled-features/src/inbound-mail-foundation/connect-route-responses.ts +26 -0
  45. package/packages/bundled-features/src/inbound-mail-foundation/connect-routes.ts +93 -62
  46. package/packages/bundled-features/src/inbound-mail-foundation/constants.ts +66 -3
  47. package/packages/bundled-features/src/inbound-mail-foundation/content-refetch.ts +137 -0
  48. package/packages/bundled-features/src/inbound-mail-foundation/describe-error.ts +5 -0
  49. package/packages/bundled-features/src/inbound-mail-foundation/entities.ts +45 -2
  50. package/packages/bundled-features/src/inbound-mail-foundation/events.ts +69 -2
  51. package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +133 -118
  52. package/packages/bundled-features/src/inbound-mail-foundation/file-storage-failure.ts +23 -0
  53. package/packages/bundled-features/src/inbound-mail-foundation/handlers/account-address.ts +79 -0
  54. package/packages/bundled-features/src/inbound-mail-foundation/handlers/acknowledge-quarantine.write.ts +55 -0
  55. package/packages/bundled-features/src/inbound-mail-foundation/handlers/attach-message-content.write.ts +184 -0
  56. package/packages/bundled-features/src/inbound-mail-foundation/handlers/connect-account.write.ts +137 -84
  57. package/packages/bundled-features/src/inbound-mail-foundation/handlers/disconnect-account.write.ts +65 -45
  58. package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message-prepare.ts +177 -0
  59. package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message-schema.ts +42 -0
  60. package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +79 -160
  61. package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-accounts.query.ts +93 -30
  62. package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-messages.query.ts +46 -30
  63. package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-quarantine.query.ts +106 -0
  64. package/packages/bundled-features/src/inbound-mail-foundation/handlers/mark-credential-stored.write.ts +47 -0
  65. package/packages/bundled-features/src/inbound-mail-foundation/handlers/message-content.query.ts +99 -0
  66. package/packages/bundled-features/src/inbound-mail-foundation/handlers/message-content.ts +229 -0
  67. package/packages/bundled-features/src/inbound-mail-foundation/handlers/message-detail.query.ts +34 -0
  68. package/packages/bundled-features/src/inbound-mail-foundation/handlers/reactivate-account.write.ts +129 -0
  69. package/packages/bundled-features/src/inbound-mail-foundation/handlers/retry-quarantined-message.write.ts +49 -0
  70. package/packages/bundled-features/src/inbound-mail-foundation/handlers/scope-visibility.ts +48 -9
  71. package/packages/bundled-features/src/inbound-mail-foundation/handlers/set-trusted-authserv-id.write.ts +66 -0
  72. package/packages/bundled-features/src/inbound-mail-foundation/handlers/thread-rollup.ts +136 -0
  73. package/packages/bundled-features/src/inbound-mail-foundation/handlers/trusted-authserv-id.ts +4 -0
  74. package/packages/bundled-features/src/inbound-mail-foundation/handlers/update-account.write.ts +82 -60
  75. package/packages/bundled-features/src/inbound-mail-foundation/i18n.ts +80 -1
  76. package/packages/bundled-features/src/inbound-mail-foundation/inbound-oauth-flow.ts +48 -0
  77. package/packages/bundled-features/src/inbound-mail-foundation/index.ts +94 -15
  78. package/packages/bundled-features/src/inbound-mail-foundation/ingest-failures.ts +137 -0
  79. package/packages/bundled-features/src/inbound-mail-foundation/ingest-payload.ts +46 -0
  80. package/packages/bundled-features/src/inbound-mail-foundation/mail-roles.ts +132 -0
  81. package/packages/bundled-features/src/inbound-mail-foundation/normalize-inbound-message.ts +119 -0
  82. package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +26 -8
  83. package/packages/bundled-features/src/inbound-mail-foundation/outbound-attachment-access.ts +28 -0
  84. package/packages/bundled-features/src/inbound-mail-foundation/outbound-attachments.ts +111 -0
  85. package/packages/bundled-features/src/inbound-mail-foundation/outbound-mail.ts +19 -1
  86. package/packages/bundled-features/src/inbound-mail-foundation/probe.ts +2 -1
  87. package/packages/bundled-features/src/inbound-mail-foundation/projection.ts +33 -2
  88. package/packages/bundled-features/src/inbound-mail-foundation/provider-errors.ts +74 -0
  89. package/packages/bundled-features/src/inbound-mail-foundation/quarantine-entities.ts +56 -0
  90. package/packages/bundled-features/src/inbound-mail-foundation/quarantine-header-import.ts +63 -0
  91. package/packages/bundled-features/src/inbound-mail-foundation/quarantine-retry.ts +137 -0
  92. package/packages/bundled-features/src/inbound-mail-foundation/quarantine-screens.ts +66 -0
  93. package/packages/bundled-features/src/inbound-mail-foundation/quarantine.ts +72 -0
  94. package/packages/bundled-features/src/inbound-mail-foundation/retention-job.ts +78 -0
  95. package/packages/bundled-features/src/inbound-mail-foundation/retention-sweep.ts +43 -8
  96. package/packages/bundled-features/src/inbound-mail-foundation/screens.ts +239 -79
  97. package/packages/bundled-features/src/inbound-mail-foundation/send-from-account.ts +24 -5
  98. package/packages/bundled-features/src/inbound-mail-foundation/sender-auth.ts +215 -0
  99. package/packages/bundled-features/src/inbound-mail-foundation/supervisor-ingest.ts +311 -0
  100. package/packages/bundled-features/src/inbound-mail-foundation/sync-account-now.ts +61 -0
  101. package/packages/bundled-features/src/inbound-mail-foundation/sync-cursor-store.ts +79 -0
  102. package/packages/bundled-features/src/inbound-mail-foundation/syncable-accounts.ts +123 -0
  103. package/packages/bundled-features/src/inbound-mail-foundation/tenant-destroy-hook.ts +5 -1
  104. package/packages/bundled-features/src/inbound-mail-foundation/types.ts +76 -105
  105. package/packages/bundled-features/src/inbound-mail-foundation/wake-consumer.ts +33 -0
  106. package/packages/bundled-features/src/inbound-mail-foundation/watch-catch-up.ts +68 -0
  107. package/packages/bundled-features/src/inbound-mail-foundation/watch-lease.ts +124 -0
  108. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor-types.ts +87 -0
  109. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +143 -433
  110. package/packages/bundled-features/src/inbound-mail-foundation/web/client-plugin.ts +73 -0
  111. package/packages/bundled-features/src/inbound-mail-foundation/web/constants.ts +7 -0
  112. package/packages/bundled-features/src/inbound-mail-foundation/web/index.ts +5 -0
  113. package/packages/bundled-features/src/inbound-provider-imap/changes.json +42 -0
  114. package/packages/bundled-features/src/inbound-provider-imap/connect-mailbox.ts +155 -63
  115. package/packages/bundled-features/src/inbound-provider-imap/connect-my-mailbox.ts +45 -0
  116. package/packages/bundled-features/src/inbound-provider-imap/constants.ts +14 -1
  117. package/packages/bundled-features/src/inbound-provider-imap/credential-document.ts +2 -1
  118. package/packages/bundled-features/src/inbound-provider-imap/download-batch.ts +38 -0
  119. package/packages/bundled-features/src/inbound-provider-imap/feature.ts +75 -44
  120. package/packages/bundled-features/src/inbound-provider-imap/fetch-content.ts +84 -0
  121. package/packages/bundled-features/src/inbound-provider-imap/i18n.ts +28 -2
  122. package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +35 -84
  123. package/packages/bundled-features/src/inbound-provider-imap/index.ts +1 -2
  124. package/packages/bundled-features/src/inbound-provider-imap/mime-content.ts +85 -0
  125. package/packages/bundled-features/src/inbound-provider-imap/parse-message.ts +149 -0
  126. package/packages/bundled-features/src/inbound-provider-imap/reconnect-account.ts +45 -0
  127. package/packages/bundled-features/src/inbound-provider-imap/reconnect-mailbox.ts +103 -0
  128. package/packages/bundled-features/src/inbound-provider-imap/reconnect-prefill.query.ts +49 -0
  129. package/packages/bundled-features/src/inbound-provider-imap/screens.ts +209 -42
  130. package/packages/bundled-features/src/inbound-provider-imap/send-mail.ts +11 -2
  131. package/packages/bundled-features/src/inbound-provider-imap/sender-auth-headers.ts +36 -0
  132. package/packages/bundled-features/src/inbound-provider-inmemory/feature.ts +75 -3
  133. package/packages/bundled-features/src/inbound-provider-inmemory/index.ts +6 -0
  134. package/packages/bundled-features/src/jobs/changes.json +18 -0
  135. package/packages/bundled-features/src/managed-pages/web/i18n.ts +1 -0
  136. package/packages/bundled-features/src/subscription-paddle/changes.json +15 -0
  137. package/packages/bundled-features/src/subscription-paddle/checkout-config.query.ts +17 -0
  138. package/packages/bundled-features/src/subscription-paddle/checkout-methods.ts +107 -0
  139. package/packages/bundled-features/src/subscription-paddle/checkout-page.ts +154 -0
  140. package/packages/bundled-features/src/subscription-paddle/client.ts +78 -0
  141. package/packages/bundled-features/src/subscription-paddle/constants.ts +32 -0
  142. package/packages/bundled-features/src/subscription-paddle/feature.ts +265 -0
  143. package/packages/bundled-features/src/subscription-paddle/index.ts +17 -0
  144. package/packages/bundled-features/src/subscription-paddle/plan-switch-methods.ts +135 -0
  145. package/packages/bundled-features/src/subscription-paddle/price-methods.ts +148 -0
  146. package/packages/bundled-features/src/subscription-paddle/runtime.ts +203 -0
  147. package/packages/bundled-features/src/subscription-paddle/subscription-state.ts +73 -0
  148. package/packages/bundled-features/src/subscription-paddle/tenant-binding.ts +34 -0
  149. package/packages/bundled-features/src/subscription-paddle/verify-webhook.ts +61 -0
  150. package/packages/bundled-features/src/subscription-paddle/webhook-events.ts +172 -0
  151. package/packages/bundled-features/src/template-resolver/changes.json +12 -0
  152. package/packages/bundled-features/src/template-resolver/handlers/collection-item.query.ts +2 -2
  153. package/packages/bundled-features/src/template-resolver/handlers/collection-list.query.ts +6 -2
  154. package/packages/bundled-features/src/template-resolver/handlers/collection-shared.ts +25 -0
  155. package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +218 -117
  156. package/packages/bundled-features/src/template-resolver/web/i18n.ts +1 -2
  157. package/packages/bundled-features/src/user-data-rights-defaults/hooks/file-ref.userdata-hook.ts +2 -1
  158. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-constants.ts +4 -0
  159. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +11 -5
  160. package/samples/apps/use-all-bundled/e2e-billing-gallery/server.ts +2 -2
  161. package/samples/apps/use-all-bundled/feature-manifest.json +89 -39
  162. package/samples/apps/use-all-bundled/kumiko/migrations/0056_inbound-mail-quarantine-retry.rebuild.json +4 -0
  163. package/samples/apps/use-all-bundled/package.json +1 -0
  164. package/samples/apps/use-all-bundled/playwright.billing-docs.config.ts +15 -0
  165. package/samples/apps/use-all-bundled/src/app/cap-overview-caps.ts +16 -2
  166. package/samples/apps/use-all-bundled/src/app/cap-overview-labels-feature.ts +2 -1
  167. package/samples/apps/use-all-bundled/src/app/client.tsx +2 -0
  168. package/samples/apps/use-all-bundled/src/app/screens-feature.ts +14 -2
  169. package/samples/apps/use-all-bundled/src/app/seed.ts +4 -1
  170. package/samples/apps/use-all-bundled/src/run-config.ts +7 -5
  171. package/samples/recipes/cross-feature-events/README.md +10 -0
  172. package/samples/recipes/cross-feature-events/src/feature.ts +14 -0
  173. package/samples/recipes/dashboard-stats/README.md +8 -0
  174. package/samples/recipes/dashboard-stats/src/feature.ts +91 -1
  175. package/samples/recipes/env-composition/README.md +2 -2
  176. package/samples/recipes/env-composition/src/env.ts +2 -2
  177. package/samples/recipes/inbound-mail-triage/README.md +12 -0
  178. package/samples/recipes/pdf-audit-export/src/feature.ts +71 -48
  179. package/packages/bundled-features/src/subscription-dodo/changes.json +0 -14
  180. package/packages/bundled-features/src/subscription-dodo/client.ts +0 -45
  181. package/packages/bundled-features/src/subscription-dodo/constants.ts +0 -24
  182. package/packages/bundled-features/src/subscription-dodo/feature.ts +0 -207
  183. package/packages/bundled-features/src/subscription-dodo/index.ts +0 -16
  184. package/packages/bundled-features/src/subscription-dodo/plugin-methods.ts +0 -345
  185. package/packages/bundled-features/src/subscription-dodo/runtime.ts +0 -126
  186. package/packages/bundled-features/src/subscription-dodo/verify-webhook.ts +0 -262
@@ -1,11 +1,21 @@
1
1
  import { ACTION_FORM_ENTITY } from "@cosmicdrift/kumiko-framework/ui-types";
2
2
  import { fieldLabelKey } from "@cosmicdrift/kumiko-renderer/headless";
3
- import { INBOUND_MAIL_FOUNDATION_FEATURE, InboundMailAccountStatuses } from "./constants.ts";
3
+ import {
4
+ INBOUND_MAIL_FOUNDATION_FEATURE,
5
+ InboundMailAccountStatuses,
6
+ InboundMailConnectErrors,
7
+ InboundMailLastErrors,
8
+ } from "./constants.ts";
4
9
  import { INBOUND_PROBE_I18N } from "./probe-keys.ts";
10
+ import { QUARANTINE_REASON_OPTION_KEY_PREFIX } from "./quarantine-screens.ts";
5
11
  import {
6
12
  MAIL_ACCOUNT_EDIT_SCREEN_ID,
13
+ MAIL_ACCOUNT_LAST_ERROR_OPTION_KEY_PREFIX,
7
14
  MAIL_ACCOUNT_LIST_SCREEN_ID,
15
+ MAIL_ACCOUNT_SENDER_CHECK_SCREEN_ID,
8
16
  MAIL_ACCOUNT_STATUS_OPTION_KEY_PREFIX,
17
+ MAIL_QUARANTINE_SCREEN_ID,
18
+ MY_MAIL_ACCOUNT_LIST_SCREEN_ID,
9
19
  } from "./screens.ts";
10
20
  import { MAIL_SEND_ERRORS } from "./send-from-account.ts";
11
21
 
@@ -15,12 +25,42 @@ const formFieldKey = (field: string): string =>
15
25
  export const INBOUND_MAIL_FOUNDATION_I18N: Readonly<Record<string, { readonly en: string }>> = {
16
26
  ...INBOUND_PROBE_I18N,
17
27
  [`screen:${MAIL_ACCOUNT_LIST_SCREEN_ID}.title`]: { en: "Mailboxes" },
28
+ [`screen:${MY_MAIL_ACCOUNT_LIST_SCREEN_ID}.title`]: { en: "Your mailboxes" },
18
29
  [`screen:${MAIL_ACCOUNT_EDIT_SCREEN_ID}.title`]: { en: "Edit mailbox" },
30
+ [`screen:${MAIL_ACCOUNT_SENDER_CHECK_SCREEN_ID}.title`]: { en: "Sender check" },
31
+ [`screen:${MAIL_QUARANTINE_SCREEN_ID}.title`]: { en: "Quarantined mails" },
19
32
  "inboundMail.accounts.col.name": { en: "Name" },
20
33
  "inboundMail.accounts.col.address": { en: "Address" },
21
34
  "inboundMail.accounts.col.provider": { en: "Provider" },
22
35
  "inboundMail.accounts.col.status": { en: "Status" },
23
36
  "inboundMail.accounts.col.connected": { en: "Connected" },
37
+ "inboundMail.accounts.col.lastError": { en: "Problem" },
38
+ "inboundMail.accounts.col.quarantined": { en: "Quarantined" },
39
+ [`${MAIL_ACCOUNT_LAST_ERROR_OPTION_KEY_PREFIX}${InboundMailLastErrors.connectionFailed}`]: {
40
+ en: "The mail server could not be reached. Syncing is retried automatically.",
41
+ },
42
+ [`${MAIL_ACCOUNT_LAST_ERROR_OPTION_KEY_PREFIX}${InboundMailLastErrors.rateLimited}`]: {
43
+ en: "The mail server is limiting requests. Syncing is retried automatically.",
44
+ },
45
+ [`${MAIL_ACCOUNT_LAST_ERROR_OPTION_KEY_PREFIX}${InboundMailLastErrors.messageQuarantined}`]: {
46
+ en: "A mail could not be imported and was set aside. Confirm the quarantine once you have looked into it.",
47
+ },
48
+ [`${MAIL_ACCOUNT_LAST_ERROR_OPTION_KEY_PREFIX}${InboundMailLastErrors.fileStorageUnavailable}`]: {
49
+ en: "File storage is not available, so attachments and originals are not saved.",
50
+ },
51
+ [`${MAIL_ACCOUNT_LAST_ERROR_OPTION_KEY_PREFIX}${InboundMailLastErrors.encryptionUnavailable}`]: {
52
+ en: "The key store is unavailable. New mails are fetched again in the next interval.",
53
+ },
54
+ [`${MAIL_ACCOUNT_LAST_ERROR_OPTION_KEY_PREFIX}${InboundMailLastErrors.ingestUnavailable}`]: {
55
+ en: "Mails cannot be imported right now. The next interval tries again.",
56
+ },
57
+ [InboundMailConnectErrors.addressAlreadyConnected.i18nKey]: {
58
+ en: "This mailbox is already connected.",
59
+ },
60
+ [InboundMailConnectErrors.addressDisconnected.i18nKey]: {
61
+ en: "This mailbox was disconnected before. Use Reactivate in the mailbox list to keep its history.",
62
+ },
63
+ "inboundMail.accounts.acknowledgeQuarantine": { en: "Confirm quarantine" },
24
64
  [`${MAIL_ACCOUNT_STATUS_OPTION_KEY_PREFIX}${InboundMailAccountStatuses.active}`]: {
25
65
  en: "Active",
26
66
  },
@@ -34,8 +74,41 @@ export const INBOUND_MAIL_FOUNDATION_I18N: Readonly<Record<string, { readonly en
34
74
  en: "Disconnected",
35
75
  },
36
76
  "inboundMail.accounts.connectImap": { en: "Connect mailbox" },
77
+ "inboundMail.accounts.reactivateOauth.confirm": {
78
+ en: "You are taken to the provider's sign-in page. After signing in, the mailbox fetches mail again and keeps its history.",
79
+ },
80
+ "inboundMail.accounts.reconnect.invalidRow": {
81
+ en: "This mailbox cannot be reactivated from here.",
82
+ },
83
+ "inboundMail.accounts.reactivate": { en: "Reactivate" },
84
+ "inboundMail.accounts.quarantine": { en: "Quarantine" },
85
+ [InboundMailConnectErrors.credentialRequired.i18nKey]: {
86
+ en: "This mailbox has no stored sign-in. Enter the password again to reactivate it.",
87
+ },
88
+ "inboundMail.quarantine.col.sender": { en: "Sender" },
89
+ "inboundMail.quarantine.col.subject": { en: "Subject" },
90
+ "inboundMail.quarantine.col.reason": { en: "Reason" },
91
+ "inboundMail.quarantine.col.mailbox": { en: "Mailbox" },
92
+ "inboundMail.quarantine.col.date": { en: "Set aside" },
93
+ "inboundMail.quarantine.retry": { en: "Fetch again" },
94
+ "inboundMail.quarantine.acknowledge": { en: "Confirm" },
95
+ [`${QUARANTINE_REASON_OPTION_KEY_PREFIX}unparseable`]: {
96
+ en: "The mail could not be read.",
97
+ },
98
+ [`${QUARANTINE_REASON_OPTION_KEY_PREFIX}invalid_message`]: {
99
+ en: "The mail is incomplete or invalid.",
100
+ },
101
+ [`${QUARANTINE_REASON_OPTION_KEY_PREFIX}ingest_failed`]: {
102
+ en: "Importing the mail failed repeatedly.",
103
+ },
37
104
  "inboundMail.accounts.edit": { en: "Rename" },
38
105
  "inboundMail.accounts.edit.submit": { en: "Save" },
106
+ "inboundMail.accounts.senderCheck": { en: "Sender check" },
107
+ "inboundMail.accounts.senderCheck.submit": { en: "Save" },
108
+ "inboundMail.accounts.senderCheckHint": {
109
+ en: "Identifier of the mail server from the Authentication-Results header, for example mx.example.com. Only with it the system checks whether a sender is genuine. Leave empty to turn the check off.",
110
+ },
111
+ [formFieldKey("trustedAuthservId")]: { en: "Mail server ID" },
39
112
  "inboundMail.accounts.disconnect": { en: "Disconnect" },
40
113
  "inboundMail.accounts.disconnect.confirm": {
41
114
  en: "Disconnect this mailbox? No new mail is fetched; stored mail is kept.",
@@ -57,4 +130,10 @@ export const INBOUND_MAIL_FOUNDATION_I18N: Readonly<Record<string, { readonly en
57
130
  [MAIL_SEND_ERRORS.invalidMessage.i18nKey]: {
58
131
  en: "The mail was not sent: a recipient, the subject or the reply reference is invalid.",
59
132
  },
133
+ [MAIL_SEND_ERRORS.attachmentInvalid.i18nKey]: {
134
+ en: "The mail was not sent: an attachment is no longer available.",
135
+ },
136
+ [MAIL_SEND_ERRORS.attachmentsTooLarge.i18nKey]: {
137
+ en: "The mail was not sent: the attachments together are larger than 18 MB.",
138
+ },
60
139
  };
@@ -0,0 +1,48 @@
1
+ import type { InboundMailContext, MailAccountRecord } from "./types.ts";
2
+
3
+ export type OAuthTokenSet = {
4
+ readonly accessToken: string;
5
+ /** ISO-Instant. TTL typisch ~1h ⇒ Foundation refresht before-poll. */
6
+ readonly expiresAt: string;
7
+ readonly refreshToken?: string;
8
+ readonly scopesGranted: readonly string[];
9
+ };
10
+
11
+ export type OAuthCodeExchange = OAuthTokenSet & {
12
+ /** Mailbox address of the account that signed in, from a token claim or the provider's profile
13
+ * call. The callback refuses a mailbox whose address differs, so nobody claims someone else's. */
14
+ readonly accountAddress: string;
15
+ };
16
+
17
+ /**
18
+ * OAuth portion of the contract — OAuth providers only; imap in password
19
+ * mode omits `oauth`. The Foundation provides the routes
20
+ * (`/inbound-mail/connect`, `/inbound-mail/oauth/callback`) generically;
21
+ * providers MUST NOT register their own routes.
22
+ */
23
+ export type InboundOAuthFlow = {
24
+ /** Scopes: request receive AND send in a SINGLE consent; re-consent is costly. */
25
+ readonly scopes: {
26
+ readonly receive: readonly string[];
27
+ readonly send?: readonly string[];
28
+ };
29
+ readonly buildAuthorizeUrl: (
30
+ ctx: InboundMailContext,
31
+ p: { readonly state: string; readonly redirectUri: string },
32
+ ) => Promise<string>;
33
+ readonly exchangeCode: (
34
+ ctx: InboundMailContext,
35
+ p: { readonly code: string; readonly redirectUri: string },
36
+ ) => Promise<OAuthCodeExchange>;
37
+ readonly refreshAccessToken: (
38
+ ctx: InboundMailContext,
39
+ account: MailAccountRecord,
40
+ refreshToken: string,
41
+ /** The foundation aborts `signal` after a hard timeout while holding a
42
+ * per-account lock (a pool connection is pinned meanwhile). Providers
43
+ * MUST pass it to their token-endpoint fetch; the foundation awaits the
44
+ * provider's result and never drops a late one, because that could lose
45
+ * a rotated refresh token. */
46
+ opts?: { readonly signal?: AbortSignal },
47
+ ) => Promise<OAuthTokenSet>;
48
+ };
@@ -1,5 +1,10 @@
1
1
  // Public API of the inbound-mail-foundation bundled-feature.
2
2
 
3
+ export {
4
+ type MailAccountWakeListener,
5
+ registerMailAccountWakeListener,
6
+ wakeMailAccount,
7
+ } from "./account-wake.ts";
3
8
  export {
4
9
  inboundMessageAggregateId,
5
10
  mailThreadAggregateId,
@@ -9,15 +14,23 @@ export {
9
14
  type InboundMailConnectRoutesOptions,
10
15
  } from "./connect-routes.ts";
11
16
  export {
17
+ IMAP_PROVIDER_KEY,
18
+ INBOUND_MAIL_CONNECT_PATH,
12
19
  INBOUND_MAIL_FOUNDATION_FEATURE,
13
20
  INBOUND_MAIL_PROVIDER_EXTENSION,
21
+ INBOUND_MAIL_RECONNECT_OAUTH_ACTION,
14
22
  type InboundMailAccountStatus,
15
23
  InboundMailAccountStatuses,
16
24
  type InboundMailAuthMethod,
17
25
  InboundMailAuthMethods,
26
+ InboundMailConnectErrors,
18
27
  InboundMailFoundationHandlers,
19
28
  InboundMailFoundationQueries,
29
+ type InboundMailLastError,
30
+ InboundMailLastErrors,
20
31
  inboundCredentialSecretKey,
32
+ type MailAccountReactivationPath,
33
+ MailAccountReactivationPaths,
21
34
  } from "./constants.ts";
22
35
  export {
23
36
  INBOUND_MESSAGE_PII_FIELDS,
@@ -33,10 +46,14 @@ export {
33
46
  } from "./entities.ts";
34
47
  export {
35
48
  INBOUND_MESSAGE_AGGREGATE_TYPE,
49
+ INBOUND_MESSAGE_CONTENT_FETCHED_EVENT_QN,
50
+ INBOUND_MESSAGE_CONTENT_FETCHED_EVENT_SHORT,
36
51
  INBOUND_MESSAGE_RECEIVED_EVENT_QN,
37
52
  INBOUND_MESSAGE_RECEIVED_EVENT_SHORT,
53
+ type InboundMessageContentFetchedEventPayload,
38
54
  type InboundMessageEventHeaders,
39
55
  type InboundMessageEventPayload,
56
+ inboundMessageContentFetchedEventPayloadSchema,
40
57
  inboundMessageEventPayloadSchema,
41
58
  MAIL_ACCOUNT_AGGREGATE_TYPE,
42
59
  MAIL_ACCOUNT_CONNECTED_EVENT_QN,
@@ -55,6 +72,36 @@ export {
55
72
  mailThreadEventPayloadSchema,
56
73
  } from "./events.ts";
57
74
  export { createInboundMailFoundationFeature } from "./feature.ts";
75
+ export { type AddressMatch, findMailboxByAddress } from "./handlers/account-address.ts";
76
+ export {
77
+ type InboundMessageContent,
78
+ loadInboundMessageContent,
79
+ } from "./handlers/message-content.query.ts";
80
+ export { isMailAdmin, mayReactivateMailbox } from "./handlers/scope-visibility.ts";
81
+ export type { InboundOAuthFlow, OAuthCodeExchange, OAuthTokenSet } from "./inbound-oauth-flow.ts";
82
+ export { ingestFailureEntity, ingestFailureTable } from "./ingest-failures.ts";
83
+ export {
84
+ assertRolesMatchFoundation,
85
+ DEFAULT_INBOUND_MAIL_ADMIN_ROLES,
86
+ DEFAULT_INBOUND_MAIL_USER_ROLES,
87
+ type InboundMailRoleOptions,
88
+ type InboundMailRoles,
89
+ readInboundMailRoles,
90
+ resolveInboundMailRoles,
91
+ roleDedupeOptions,
92
+ SYSTEM_DISPATCHED_MAIL_HANDLERS,
93
+ } from "./mail-roles.ts";
94
+ export {
95
+ INBOUND_ATTACHMENT_MAX_BYTES,
96
+ INBOUND_ATTACHMENTS_MAX_COUNT,
97
+ INBOUND_BODY_HTML_MAX_CHARS,
98
+ INBOUND_BODY_TEXT_MAX_CHARS,
99
+ INBOUND_MAIL_ATTACHMENTS_MAX_BYTES,
100
+ INBOUND_MISSING_SENDER,
101
+ INBOUND_SKIPPED_ATTACHMENTS_MAX_COUNT,
102
+ type NormalizedInboundMessage,
103
+ normalizeRawInboundMessage,
104
+ } from "./normalize-inbound-message.ts";
58
105
  export {
59
106
  createOAuthAccessTokenManager,
60
107
  inboundRefreshTokenSecretOptions,
@@ -68,29 +115,57 @@ export {
68
115
  type VerifyOAuthStateResult,
69
116
  verifyOAuthState,
70
117
  } from "./oauth-state.ts";
118
+ export { readableFileName } from "./outbound-attachments.ts";
71
119
  export {
72
120
  isMailSendNotConfiguredError,
73
121
  isMailSendRejectedError,
74
122
  MailSendNotConfiguredError,
75
123
  MailSendRejectedError,
124
+ OUTBOUND_ATTACHMENTS_MAX_BYTES,
125
+ type OutboundAttachment,
76
126
  type OutboundMailMessage,
77
127
  type OutboundMailSent,
78
128
  outboundMailMessageSchema,
129
+ type ProviderSentMail,
79
130
  } from "./outbound-mail.ts";
80
131
  export {
81
132
  inboundMessagesProjectionTable,
82
133
  mailAccountsProjectionTable,
83
134
  mailThreadsProjectionTable,
84
135
  } from "./projection.ts";
136
+ export {
137
+ InboundAuthError,
138
+ InboundCredentialNotStoredError,
139
+ InboundCursorInvalidError,
140
+ InboundRateLimitError,
141
+ InboundTransientError,
142
+ isInboundAuthError,
143
+ isInboundCredentialNotStoredError,
144
+ isInboundCursorInvalidError,
145
+ isInboundRateLimitError,
146
+ isInboundTransientError,
147
+ } from "./provider-errors.ts";
85
148
  export {
86
149
  resolveInboundProviderForAccount,
87
150
  resolveInboundProviderForKey,
88
151
  } from "./provider-factory.ts";
152
+ export {
153
+ QUARANTINE_REASON_CODES,
154
+ type QuarantineReasonCode,
155
+ quarantineEntity,
156
+ quarantineTable,
157
+ } from "./quarantine-entities.ts";
89
158
  export {
90
159
  IMAP_CONNECT_SCREEN_ID,
160
+ IMAP_RECONNECT_SCREEN_ID,
91
161
  MAIL_ACCOUNT_EDIT_SCREEN_ID,
92
162
  MAIL_ACCOUNT_LIST_SCREEN_ID,
93
163
  MAIL_ACCOUNT_LIST_SCREEN_QN,
164
+ MAIL_QUARANTINE_SCREEN_ID,
165
+ MY_IMAP_CONNECT_SCREEN_ID,
166
+ MY_IMAP_RECONNECT_SCREEN_ID,
167
+ MY_MAIL_ACCOUNT_LIST_SCREEN_ID,
168
+ MY_MAIL_ACCOUNT_LIST_SCREEN_QN,
94
169
  } from "./screens.ts";
95
170
  export {
96
171
  MAIL_SEND_ERRORS,
@@ -100,26 +175,30 @@ export {
100
175
  sendMailFromAccount,
101
176
  } from "./send-from-account.ts";
102
177
  export {
103
- InboundAuthError,
104
- InboundCursorInvalidError,
178
+ fromDomainOf,
179
+ isSenderAuthenticated,
180
+ normalizeTrustedAuthservId,
181
+ parseSenderAuthentication,
182
+ SENDER_AUTH_VERDICTS,
183
+ type SenderAuthentication,
184
+ type SenderAuthVerdict,
185
+ senderAuthenticationSchema,
186
+ } from "./sender-auth.ts";
187
+ export {
188
+ type InboundContentFetchResult,
105
189
  type InboundFetchResult,
106
190
  type InboundMailContext,
107
191
  type InboundMailProviderPlugin,
108
- type InboundOAuthFlow,
109
- InboundRateLimitError,
110
- InboundTransientError,
111
- isInboundAuthError,
112
- isInboundCursorInvalidError,
113
192
  isInboundMailProviderPlugin,
114
- isInboundRateLimitError,
115
- isInboundTransientError,
116
193
  type MailAccountRecord,
117
- type OAuthTokenSet,
194
+ type RawInboundAttachment,
118
195
  type RawInboundMessage,
196
+ type RejectedInboundMessage,
197
+ type SkippedInboundAttachment,
119
198
  type SyncCursorPayload,
120
199
  } from "./types.ts";
121
- export {
122
- createInboundMailSupervisor,
123
- type InboundMailSupervisor,
124
- type InboundMailSupervisorDeps,
125
- } from "./watch-supervisor.ts";
200
+ export { createInboundMailSupervisor } from "./watch-supervisor.ts";
201
+ export type {
202
+ InboundMailSupervisor,
203
+ InboundMailSupervisorDeps,
204
+ } from "./watch-supervisor-types.ts";
@@ -0,0 +1,137 @@
1
+ // Failure counter of mails the ingest could not take. Persisted, so every worker replica and every
2
+ // restart sees the same count; a poison mail therefore reaches quarantine instead of blocking the
3
+ // cursor of its mailbox forever. Rows carry ids only, no mail content.
4
+
5
+ import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
6
+ import {
7
+ deleteMany,
8
+ deriveEntityTableMeta,
9
+ fetchOne,
10
+ insertOnConflictDoNothing,
11
+ updateMany,
12
+ } from "@cosmicdrift/kumiko-framework/db";
13
+ import {
14
+ access,
15
+ createEntity,
16
+ createNumberField,
17
+ createTextField,
18
+ createTimestampField,
19
+ } from "@cosmicdrift/kumiko-framework/engine";
20
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
21
+
22
+ export const MAX_COUNTED_INGEST_FAILURES = 3;
23
+ const COMPARE_AND_SET_ATTEMPTS = 3;
24
+
25
+ const technicalReference = {
26
+ access: { write: access.privileged },
27
+ personal: false,
28
+ reason: "technical_reference",
29
+ } as const;
30
+
31
+ export const ingestFailureEntity = createEntity({
32
+ table: "store_mail_ingest_failures",
33
+ softDelete: false,
34
+ indexes: [{ unique: true, columns: ["tenantId", "accountId", "providerMessageId"] }],
35
+ fields: {
36
+ accountId: createTextField({ required: true, maxLength: 36, ...technicalReference }),
37
+ providerMessageId: createTextField({ required: true, maxLength: 500, ...technicalReference }),
38
+ failureCount: createNumberField({ required: true, integer: true, default: 0 }),
39
+ // Double precision: epoch milliseconds divided by a short poll interval overflow a 32-bit integer.
40
+ lastCountedBucket: createNumberField({ required: true, default: 0 }),
41
+ lastFailedAt: createTimestampField({ required: true, access: { write: access.privileged } }),
42
+ },
43
+ });
44
+
45
+ export const ingestFailureTable = deriveEntityTableMeta(
46
+ "mail-ingest-failure",
47
+ ingestFailureEntity,
48
+ { source: "unmanaged" },
49
+ );
50
+
51
+ export type IngestFailureTarget = {
52
+ readonly tenantId: string;
53
+ readonly accountId: string;
54
+ readonly providerMessageId: string;
55
+ };
56
+
57
+ type CounterRow = { readonly failureCount: number };
58
+
59
+ /** Bucket of one poll interval: replicas and overlapping polls inside the same interval count a mail once. */
60
+ export function countBucketOf(now: Temporal.Instant, pollIntervalMs: number): number {
61
+ return Math.floor(now.epochMilliseconds / pollIntervalMs);
62
+ }
63
+
64
+ export function currentCountBucket(
65
+ now: (() => Temporal.Instant) | undefined,
66
+ pollIntervalMs: number,
67
+ ): number {
68
+ return countBucketOf((now ?? Temporal.Now.instant)(), pollIntervalMs);
69
+ }
70
+
71
+ /** The new failure count, or null when this bucket was already counted (or another worker won the race). */
72
+ export async function countIngestFailure(
73
+ db: DbConnection,
74
+ target: IngestFailureTarget,
75
+ bucket: number,
76
+ ): Promise<number | null> {
77
+ const key = { accountId: target.accountId, providerMessageId: target.providerMessageId };
78
+ const lastFailedAt = Temporal.Now.instant().toString();
79
+ const inserted = await insertOnConflictDoNothing(
80
+ db,
81
+ ingestFailureTable,
82
+ {
83
+ id: crypto.randomUUID(),
84
+ tenantId: target.tenantId,
85
+ ...key,
86
+ failureCount: 1,
87
+ lastCountedBucket: bucket,
88
+ lastFailedAt,
89
+ },
90
+ { conflictKeys: ["tenantId", "accountId", "providerMessageId"] },
91
+ );
92
+ if (inserted) return 1;
93
+ for (let attempt = 0; attempt < COMPARE_AND_SET_ATTEMPTS; attempt++) {
94
+ const row = await fetchOne<CounterRow>(db, ingestFailureTable, key);
95
+ if (!row) return null;
96
+ const next = row.failureCount + 1;
97
+ const updated = await updateMany(
98
+ db,
99
+ ingestFailureTable,
100
+ { failureCount: next, lastCountedBucket: bucket, lastFailedAt },
101
+ { ...key, failureCount: row.failureCount, lastCountedBucket: { lt: bucket } },
102
+ );
103
+ if (updated.length > 0) return next;
104
+ const current = await fetchOne<CounterRow & { readonly lastCountedBucket: number }>(
105
+ db,
106
+ ingestFailureTable,
107
+ key,
108
+ );
109
+ // skip: this bucket is counted already, a second count would double the failure of one interval
110
+ if (!current || current.lastCountedBucket >= bucket) return null;
111
+ }
112
+ return null;
113
+ }
114
+
115
+ export async function hasCountedIngestFailure(
116
+ db: DbConnection,
117
+ target: IngestFailureTarget,
118
+ ): Promise<boolean> {
119
+ const row = await fetchOne<CounterRow>(db, ingestFailureTable, {
120
+ accountId: target.accountId,
121
+ providerMessageId: target.providerMessageId,
122
+ });
123
+ return row !== undefined;
124
+ }
125
+
126
+ export async function clearIngestFailures(
127
+ db: DbConnection,
128
+ accountId: string,
129
+ providerMessageIds: readonly string[],
130
+ ): Promise<void> {
131
+ // skip: nothing to clear
132
+ if (providerMessageIds.length === 0) return;
133
+ await deleteMany(db, ingestFailureTable, {
134
+ accountId,
135
+ providerMessageId: { in: [...providerMessageIds] },
136
+ });
137
+ }
@@ -0,0 +1,46 @@
1
+ // The ingest-message payload built from a provider message. Shared by the supervisor's regular
2
+ // ingest, the header import of quarantined mails and the quarantine retry.
3
+
4
+ import type { NormalizedInboundMessage } from "./normalize-inbound-message.ts";
5
+ import type { MailAccountRecord } from "./types.ts";
6
+
7
+ export function ingestPayloadOf(
8
+ account: MailAccountRecord,
9
+ msg: NormalizedInboundMessage,
10
+ options: {
11
+ readonly providerCursor: string;
12
+ readonly bodyRef: string;
13
+ readonly contentUnavailableReason?: "quarantined";
14
+ },
15
+ ): Record<string, unknown> {
16
+ return {
17
+ accountId: account.id,
18
+ ownerUserId: account.ownerUserId,
19
+ providerName: account.provider,
20
+ providerMessageId: msg.providerMessageId,
21
+ messageIdHeader: msg.messageIdHeader,
22
+ providerThreadId: msg.providerThreadId,
23
+ references: msg.references,
24
+ from: msg.from,
25
+ to: msg.to,
26
+ cc: msg.cc,
27
+ subject: msg.subject,
28
+ snippet: msg.snippet,
29
+ receivedAtIso: msg.receivedAtIso,
30
+ bodyRef: options.bodyRef,
31
+ scope: msg.scope,
32
+ providerCursor: options.providerCursor,
33
+ bodyText: msg.bodyText,
34
+ bodyTextTruncated: msg.bodyTextTruncated,
35
+ ...(msg.rawMime != null && { rawMime: msg.rawMime }),
36
+ ...(msg.bodyHtml !== null && { bodyHtml: msg.bodyHtml }),
37
+ attachments: msg.attachments,
38
+ skippedAttachments: msg.skippedAttachments,
39
+ ...(msg.senderAuthentication !== undefined && {
40
+ senderAuthentication: msg.senderAuthentication,
41
+ }),
42
+ ...(options.contentUnavailableReason !== undefined && {
43
+ contentUnavailableReason: options.contentUnavailableReason,
44
+ }),
45
+ };
46
+ }
@@ -0,0 +1,132 @@
1
+ import { ROLES } from "@cosmicdrift/kumiko-framework/auth";
2
+ import type { FeatureDefinition, Registry } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
4
+ import { INBOUND_MAIL_FOUNDATION_FEATURE, InboundMailFoundationHandlers } from "./constants.ts";
5
+
6
+ export type InboundMailRoleOptions = {
7
+ /** Roles that use their own and the shared mailboxes. Admins are always members too. */
8
+ readonly userRoles?: readonly string[];
9
+ /** Roles that administer shared mailboxes and see the metadata of personal ones. */
10
+ readonly adminRoles?: readonly string[];
11
+ };
12
+
13
+ export type InboundMailRoles = {
14
+ readonly userRoles: readonly string[];
15
+ readonly adminRoles: readonly string[];
16
+ /** admin ∪ user, deduplicated: everyone who may touch the mail feature at all. */
17
+ readonly memberRoles: readonly string[];
18
+ };
19
+
20
+ export const DEFAULT_INBOUND_MAIL_USER_ROLES = ["User"] as const;
21
+ export const DEFAULT_INBOUND_MAIL_ADMIN_ROLES = [ROLES.TenantAdmin, ROLES.SystemAdmin] as const;
22
+
23
+ function validatedRoles(optionName: string, roles: readonly string[]): readonly string[] {
24
+ if (roles.length === 0) {
25
+ throw new Error(`createInboundMailFoundationFeature: ${optionName} must not be empty.`);
26
+ }
27
+ if (roles.some((role) => role.trim() === "")) {
28
+ throw new Error(
29
+ `createInboundMailFoundationFeature: ${optionName} must not contain empty role names.`,
30
+ );
31
+ }
32
+ if (roles.includes("anonymous")) {
33
+ throw new Error(
34
+ `createInboundMailFoundationFeature: ${optionName} must not contain "anonymous": mail is only readable by members of the tenant.`,
35
+ );
36
+ }
37
+ return [...roles];
38
+ }
39
+
40
+ // Handlers the supervisor, the account health, the OAuth callback and the refresh job dispatch as the
41
+ // system user (SystemAdmin). Every new system dispatch must target one of these; a test scans the
42
+ // sources for it.
43
+ export const SYSTEM_DISPATCHED_MAIL_HANDLERS: readonly string[] = [
44
+ InboundMailFoundationHandlers.ingestMessage,
45
+ InboundMailFoundationHandlers.connectAccount,
46
+ InboundMailFoundationHandlers.updateAccount,
47
+ InboundMailFoundationHandlers.reactivateAccount,
48
+ InboundMailFoundationHandlers.markCredentialStored,
49
+ InboundMailFoundationHandlers.attachMessageContent,
50
+ ];
51
+
52
+ // An app whose adminRoles/memberRoles leave SystemAdmin out would see every system dispatch refused.
53
+ // Only handler access lists use this; isMailAdmin and screens keep the configured roles.
54
+ export function accessRolesOf(handlerQn: string, roles: readonly string[]): string[] {
55
+ return SYSTEM_DISPATCHED_MAIL_HANDLERS.includes(handlerQn)
56
+ ? [...new Set([...roles, ROLES.SystemAdmin])]
57
+ : [...roles];
58
+ }
59
+
60
+ export function resolveInboundMailRoles(options: InboundMailRoleOptions = {}): InboundMailRoles {
61
+ const userRoles = validatedRoles(
62
+ "userRoles",
63
+ options.userRoles ?? DEFAULT_INBOUND_MAIL_USER_ROLES,
64
+ );
65
+ const adminRoles = validatedRoles(
66
+ "adminRoles",
67
+ options.adminRoles ?? DEFAULT_INBOUND_MAIL_ADMIN_ROLES,
68
+ );
69
+ return { userRoles, adminRoles, memberRoles: [...new Set([...adminRoles, ...userRoles])] };
70
+ }
71
+
72
+ // dedupeOptions accept primitives only: equal role sets collapse, differing ones fail at boot.
73
+ export function roleDedupeOptions(roles: InboundMailRoles): Readonly<Record<string, string>> {
74
+ return {
75
+ userRoles: [...roles.userRoles].sort().join(","),
76
+ adminRoles: [...roles.adminRoles].sort().join(","),
77
+ };
78
+ }
79
+
80
+ function isStringArray(value: unknown): value is readonly string[] {
81
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
82
+ }
83
+
84
+ function isInboundMailRoles(value: unknown): value is InboundMailRoles {
85
+ return (
86
+ isPlainObject(value) &&
87
+ isStringArray(value["userRoles"]) &&
88
+ isStringArray(value["adminRoles"]) &&
89
+ isStringArray(value["memberRoles"])
90
+ );
91
+ }
92
+
93
+ function inboundMailRolesOfFeature(feature: FeatureDefinition): InboundMailRoles | undefined {
94
+ const exported = feature.exports;
95
+ return isPlainObject(exported) && isInboundMailRoles(exported["roles"])
96
+ ? exported["roles"]
97
+ : undefined;
98
+ }
99
+
100
+ export function readInboundMailRoles(registry: Registry): InboundMailRoles {
101
+ const feature = registry.features.get(INBOUND_MAIL_FOUNDATION_FEATURE);
102
+ return (feature && inboundMailRolesOfFeature(feature)) ?? resolveInboundMailRoles();
103
+ }
104
+
105
+ function sameRoleSet(a: readonly string[], b: readonly string[]): boolean {
106
+ return a.length === b.length && a.every((role) => b.includes(role));
107
+ }
108
+
109
+ // Provider features register forms whose dispatches the foundation's handlers answer, so roles
110
+ // that differ from the foundation's would only surface as 403s at runtime.
111
+ export function assertRolesMatchFoundation(
112
+ featureName: string,
113
+ roles: InboundMailRoles,
114
+ features: readonly FeatureDefinition[],
115
+ ): void {
116
+ const foundation = features.find((feature) => feature.name === INBOUND_MAIL_FOUNDATION_FEATURE);
117
+ const foundationRoles = foundation && inboundMailRolesOfFeature(foundation);
118
+ if (
119
+ foundationRoles !== undefined &&
120
+ !(
121
+ sameRoleSet(foundationRoles.userRoles, roles.userRoles) &&
122
+ sameRoleSet(foundationRoles.adminRoles, roles.adminRoles)
123
+ )
124
+ ) {
125
+ throw new Error(
126
+ `${featureName}: role options differ from ${INBOUND_MAIL_FOUNDATION_FEATURE} ` +
127
+ `(userRoles [${roles.userRoles.join(", ")}] vs [${foundationRoles.userRoles.join(", ")}], ` +
128
+ `adminRoles [${roles.adminRoles.join(", ")}] vs [${foundationRoles.adminRoles.join(", ")}]). ` +
129
+ `Pass the same userRoles/adminRoles to ${featureName} and ${INBOUND_MAIL_FOUNDATION_FEATURE}.`,
130
+ );
131
+ }
132
+ }