@dxos/plugin-jmap 0.10.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 (180) hide show
  1. package/LICENSE +105 -0
  2. package/dist/lib/JmapPlugin.mjs +16 -0
  3. package/dist/lib/JmapPlugin.mjs.map +1 -0
  4. package/dist/lib/apis.mjs +2 -0
  5. package/dist/lib/capabilities.mjs +17 -0
  6. package/dist/lib/capabilities.mjs.map +1 -0
  7. package/dist/lib/chunk-apis.mjs +756 -0
  8. package/dist/lib/chunk-apis.mjs.map +1 -0
  9. package/dist/lib/chunk-connector.mjs +111 -0
  10. package/dist/lib/chunk-connector.mjs.map +1 -0
  11. package/dist/lib/chunk-constants.mjs +23 -0
  12. package/dist/lib/chunk-constants.mjs.map +1 -0
  13. package/dist/lib/chunk-errors.mjs +42 -0
  14. package/dist/lib/chunk-errors.mjs.map +1 -0
  15. package/dist/lib/chunk-handler.mjs +27 -0
  16. package/dist/lib/chunk-handler.mjs.map +1 -0
  17. package/dist/lib/chunk-jmap-fixtures.mjs +102 -0
  18. package/dist/lib/chunk-jmap-fixtures.mjs.map +1 -0
  19. package/dist/lib/chunk-mail-send.mjs +17 -0
  20. package/dist/lib/chunk-mail-send.mjs.map +1 -0
  21. package/dist/lib/chunk-meta.mjs +37 -0
  22. package/dist/lib/chunk-meta.mjs.map +1 -0
  23. package/dist/lib/chunk-operation-handler.mjs +12 -0
  24. package/dist/lib/chunk-operation-handler.mjs.map +1 -0
  25. package/dist/lib/chunk-send.mjs +98 -0
  26. package/dist/lib/chunk-send.mjs.map +1 -0
  27. package/dist/lib/chunk-sync-provider.mjs +511 -0
  28. package/dist/lib/chunk-sync-provider.mjs.map +1 -0
  29. package/dist/lib/chunk-sync.mjs +37 -0
  30. package/dist/lib/chunk-sync.mjs.map +1 -0
  31. package/dist/lib/index.mjs +4 -0
  32. package/dist/lib/meta.mjs +2 -0
  33. package/dist/lib/operations.mjs +13 -0
  34. package/dist/lib/operations.mjs.map +1 -0
  35. package/dist/lib/plugin.mjs +9 -0
  36. package/dist/lib/plugin.mjs.map +1 -0
  37. package/dist/lib/services.mjs +139 -0
  38. package/dist/lib/services.mjs.map +1 -0
  39. package/dist/lib/testing/node.mjs +2 -0
  40. package/dist/lib/testing.mjs +19 -0
  41. package/dist/lib/testing.mjs.map +1 -0
  42. package/dist/lib/translations.mjs +7 -0
  43. package/dist/lib/translations.mjs.map +1 -0
  44. package/dist/types/dx.config.d.ts +28 -0
  45. package/dist/types/dx.config.d.ts.map +1 -0
  46. package/dist/types/src/JmapPlugin.d.ts +9 -0
  47. package/dist/types/src/JmapPlugin.d.ts.map +1 -0
  48. package/dist/types/src/apis/Jmap/api.d.ts +42 -0
  49. package/dist/types/src/apis/Jmap/api.d.ts.map +1 -0
  50. package/dist/types/src/apis/Jmap/index.d.ts +3 -0
  51. package/dist/types/src/apis/Jmap/index.d.ts.map +1 -0
  52. package/dist/types/src/apis/Jmap/types.d.ts +65 -0
  53. package/dist/types/src/apis/Jmap/types.d.ts.map +1 -0
  54. package/dist/types/src/apis/JmapMail/api.d.ts +223 -0
  55. package/dist/types/src/apis/JmapMail/api.d.ts.map +1 -0
  56. package/dist/types/src/apis/JmapMail/index.d.ts +4 -0
  57. package/dist/types/src/apis/JmapMail/index.d.ts.map +1 -0
  58. package/dist/types/src/apis/JmapMail/query.d.ts +26 -0
  59. package/dist/types/src/apis/JmapMail/query.d.ts.map +1 -0
  60. package/dist/types/src/apis/JmapMail/query.test.d.ts +2 -0
  61. package/dist/types/src/apis/JmapMail/query.test.d.ts.map +1 -0
  62. package/dist/types/src/apis/JmapMail/types.d.ts +290 -0
  63. package/dist/types/src/apis/JmapMail/types.d.ts.map +1 -0
  64. package/dist/types/src/apis/index.d.ts +3 -0
  65. package/dist/types/src/apis/index.d.ts.map +1 -0
  66. package/dist/types/src/apis/jmap-api.test.d.ts +2 -0
  67. package/dist/types/src/apis/jmap-api.test.d.ts.map +1 -0
  68. package/dist/types/src/capabilities/connector.d.ts +5 -0
  69. package/dist/types/src/capabilities/connector.d.ts.map +1 -0
  70. package/dist/types/src/capabilities/credential-form.d.ts +37 -0
  71. package/dist/types/src/capabilities/credential-form.d.ts.map +1 -0
  72. package/dist/types/src/capabilities/credential-form.test.d.ts +2 -0
  73. package/dist/types/src/capabilities/credential-form.test.d.ts.map +1 -0
  74. package/dist/types/src/capabilities/index.d.ts +5 -0
  75. package/dist/types/src/capabilities/index.d.ts.map +1 -0
  76. package/dist/types/src/capabilities/mail-send.d.ts +6 -0
  77. package/dist/types/src/capabilities/mail-send.d.ts.map +1 -0
  78. package/dist/types/src/capabilities/operation-handler.d.ts +5 -0
  79. package/dist/types/src/capabilities/operation-handler.d.ts.map +1 -0
  80. package/dist/types/src/constants.d.ts +19 -0
  81. package/dist/types/src/constants.d.ts.map +1 -0
  82. package/dist/types/src/errors.d.ts +103 -0
  83. package/dist/types/src/errors.d.ts.map +1 -0
  84. package/dist/types/src/index.d.ts +4 -0
  85. package/dist/types/src/index.d.ts.map +1 -0
  86. package/dist/types/src/meta.d.ts +33 -0
  87. package/dist/types/src/meta.d.ts.map +1 -0
  88. package/dist/types/src/operations/index.d.ts +3 -0
  89. package/dist/types/src/operations/index.d.ts.map +1 -0
  90. package/dist/types/src/operations/mail/mapper.d.ts +49 -0
  91. package/dist/types/src/operations/mail/mapper.d.ts.map +1 -0
  92. package/dist/types/src/operations/mail/materialize/handler.d.ts +11 -0
  93. package/dist/types/src/operations/mail/materialize/handler.d.ts.map +1 -0
  94. package/dist/types/src/operations/mail/send/handler.d.ts +4 -0
  95. package/dist/types/src/operations/mail/send/handler.d.ts.map +1 -0
  96. package/dist/types/src/operations/mail/send/index.d.ts +2 -0
  97. package/dist/types/src/operations/mail/send/index.d.ts.map +1 -0
  98. package/dist/types/src/operations/mail/sync/handler.d.ts +4 -0
  99. package/dist/types/src/operations/mail/sync/handler.d.ts.map +1 -0
  100. package/dist/types/src/operations/mail/sync/index.d.ts +3 -0
  101. package/dist/types/src/operations/mail/sync/index.d.ts.map +1 -0
  102. package/dist/types/src/operations/mail/sync/mapper.test.d.ts +2 -0
  103. package/dist/types/src/operations/mail/sync/mapper.test.d.ts.map +1 -0
  104. package/dist/types/src/operations/mail/sync/sync-e2e.test.d.ts +2 -0
  105. package/dist/types/src/operations/mail/sync/sync-e2e.test.d.ts.map +1 -0
  106. package/dist/types/src/operations/mail/sync/sync-provider.d.ts +11 -0
  107. package/dist/types/src/operations/mail/sync/sync-provider.d.ts.map +1 -0
  108. package/dist/types/src/operations/mail/sync/sync.test.d.ts +2 -0
  109. package/dist/types/src/operations/mail/sync/sync.test.d.ts.map +1 -0
  110. package/dist/types/src/operations/mail/sync/system-tags.d.ts +14 -0
  111. package/dist/types/src/operations/mail/sync/system-tags.d.ts.map +1 -0
  112. package/dist/types/src/operations/mail/tags.d.ts +12 -0
  113. package/dist/types/src/operations/mail/tags.d.ts.map +1 -0
  114. package/dist/types/src/plugin.d.ts +4 -0
  115. package/dist/types/src/plugin.d.ts.map +1 -0
  116. package/dist/types/src/services/index.d.ts +3 -0
  117. package/dist/types/src/services/index.d.ts.map +1 -0
  118. package/dist/types/src/services/jmap-credentials.d.ts +39 -0
  119. package/dist/types/src/services/jmap-credentials.d.ts.map +1 -0
  120. package/dist/types/src/services/jmap-mail-api.d.ts +109 -0
  121. package/dist/types/src/services/jmap-mail-api.d.ts.map +1 -0
  122. package/dist/types/src/testing/index.d.ts +5 -0
  123. package/dist/types/src/testing/index.d.ts.map +1 -0
  124. package/dist/types/src/testing/jmap-fixtures.d.ts +26 -0
  125. package/dist/types/src/testing/jmap-fixtures.d.ts.map +1 -0
  126. package/dist/types/src/testing/jmap-fixtures.test.d.ts +2 -0
  127. package/dist/types/src/testing/jmap-fixtures.test.d.ts.map +1 -0
  128. package/dist/types/src/testing/node.d.ts +4 -0
  129. package/dist/types/src/testing/node.d.ts.map +1 -0
  130. package/dist/types/src/testing/sync-fixture.d.ts +22 -0
  131. package/dist/types/src/testing/sync-fixture.d.ts.map +1 -0
  132. package/dist/types/src/translations.d.ts +8 -0
  133. package/dist/types/src/translations.d.ts.map +1 -0
  134. package/dist/types/tsconfig.tsbuildinfo +1 -0
  135. package/dx.config.ts +32 -0
  136. package/package.json +121 -0
  137. package/src/JmapPlugin.ts +25 -0
  138. package/src/apis/Jmap/api.ts +138 -0
  139. package/src/apis/Jmap/index.ts +6 -0
  140. package/src/apis/Jmap/types.ts +86 -0
  141. package/src/apis/JmapMail/api.ts +324 -0
  142. package/src/apis/JmapMail/index.ts +7 -0
  143. package/src/apis/JmapMail/query.test.ts +181 -0
  144. package/src/apis/JmapMail/query.ts +231 -0
  145. package/src/apis/JmapMail/types.ts +196 -0
  146. package/src/apis/index.ts +8 -0
  147. package/src/apis/jmap-api.test.ts +261 -0
  148. package/src/capabilities/connector.ts +44 -0
  149. package/src/capabilities/credential-form.test.ts +69 -0
  150. package/src/capabilities/credential-form.ts +113 -0
  151. package/src/capabilities/index.ts +24 -0
  152. package/src/capabilities/mail-send.ts +21 -0
  153. package/src/capabilities/operation-handler.ts +16 -0
  154. package/src/constants.ts +24 -0
  155. package/src/errors.ts +38 -0
  156. package/src/index.ts +7 -0
  157. package/src/meta.ts +9 -0
  158. package/src/operations/index.ts +13 -0
  159. package/src/operations/mail/mapper.ts +191 -0
  160. package/src/operations/mail/materialize/handler.ts +41 -0
  161. package/src/operations/mail/send/handler.ts +116 -0
  162. package/src/operations/mail/send/index.ts +5 -0
  163. package/src/operations/mail/sync/handler.ts +52 -0
  164. package/src/operations/mail/sync/index.ts +7 -0
  165. package/src/operations/mail/sync/mapper.test.ts +150 -0
  166. package/src/operations/mail/sync/sync-e2e.test.ts +63 -0
  167. package/src/operations/mail/sync/sync-provider.ts +481 -0
  168. package/src/operations/mail/sync/sync.test.ts +766 -0
  169. package/src/operations/mail/sync/system-tags.ts +24 -0
  170. package/src/operations/mail/tags.ts +29 -0
  171. package/src/plugin.ts +11 -0
  172. package/src/services/index.ts +6 -0
  173. package/src/services/jmap-credentials.ts +61 -0
  174. package/src/services/jmap-mail-api.ts +294 -0
  175. package/src/testing/index.ts +8 -0
  176. package/src/testing/jmap-fixtures.test.ts +107 -0
  177. package/src/testing/jmap-fixtures.ts +105 -0
  178. package/src/testing/node.ts +12 -0
  179. package/src/testing/sync-fixture.ts +39 -0
  180. package/src/translations.ts +15 -0
package/dx.config.ts ADDED
@@ -0,0 +1,32 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { Config2 } from '@dxos/app-framework/config';
6
+ import { trim } from '@dxos/util';
7
+
8
+ export default Config2.make({
9
+ plugin: {
10
+ key: 'org.dxos.plugin.jmap',
11
+ name: 'JMAP',
12
+ author: 'DXOS',
13
+ source: 'https://github.com/dxos/dxos/tree/main/packages/plugins/plugin-jmap',
14
+ description: trim`
15
+ Syncs mail from any JMAP server (RFC 8620/8621) into a local-first Mailbox. Fastmail is the
16
+ canonical provider; the session is discovered at the well-known JMAP endpoint on the host you
17
+ supply.
18
+
19
+ Headless: this plugin contributes the JMAP connector, its credential form, and the sync, send
20
+ and materialize operations. The mailbox itself, its UI, and the provider-agnostic sync harness
21
+ belong to the Inbox plugin, which this one depends on.
22
+
23
+ Credentials are a host, an account email, and a Bearer API token, validated against the live
24
+ session when the form is submitted — no OAuth. Sync is incremental: after the first run the
25
+ server's state string drives a bounded delta per run, and folder and keyword changes reconcile
26
+ against messages already in the feed.
27
+ `,
28
+ dependsOn: ['org.dxos.plugin.inbox'],
29
+ icon: { key: 'ph--envelope-simple-open--regular', hue: 'green' },
30
+ tags: ['alpha', 'connector'],
31
+ },
32
+ });
package/package.json ADDED
@@ -0,0 +1,121 @@
1
+ {
2
+ "name": "@dxos/plugin-jmap",
3
+ "version": "0.10.0",
4
+ "description": "DXOS plugin that syncs mail from a JMAP server (RFC 8620/8621) into a Mailbox.",
5
+ "homepage": "https://dxos.org",
6
+ "bugs": "https://github.com/dxos/dxos/issues",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/dxos/dxos"
10
+ },
11
+ "license": "FSL-1.1-Apache-2.0",
12
+ "author": "DXOS.org",
13
+ "sideEffects": true,
14
+ "type": "module",
15
+ "imports": {
16
+ "#apis": {
17
+ "source": "./src/apis/index.ts",
18
+ "types": "./dist/types/src/apis/index.d.ts",
19
+ "default": "./dist/lib/apis.mjs"
20
+ },
21
+ "#capabilities": {
22
+ "source": "./src/capabilities/index.ts",
23
+ "types": "./dist/types/src/capabilities/index.d.ts",
24
+ "default": "./dist/lib/capabilities.mjs"
25
+ },
26
+ "#meta": {
27
+ "source": "./src/meta.ts",
28
+ "types": "./dist/types/src/meta.d.ts",
29
+ "default": "./dist/lib/meta.mjs"
30
+ },
31
+ "#operations": {
32
+ "source": "./src/operations/index.ts",
33
+ "types": "./dist/types/src/operations/index.d.ts",
34
+ "default": "./dist/lib/operations.mjs"
35
+ },
36
+ "#plugin": {
37
+ "source": "./src/JmapPlugin.ts",
38
+ "types": "./dist/types/src/JmapPlugin.d.ts",
39
+ "default": "./dist/lib/JmapPlugin.mjs"
40
+ },
41
+ "#services": {
42
+ "source": "./src/services/index.ts",
43
+ "types": "./dist/types/src/services/index.d.ts",
44
+ "default": "./dist/lib/services.mjs"
45
+ },
46
+ "#translations": {
47
+ "source": "./src/translations.ts",
48
+ "types": "./dist/types/src/translations.d.ts",
49
+ "default": "./dist/lib/translations.mjs"
50
+ }
51
+ },
52
+ "exports": {
53
+ ".": {
54
+ "source": "./src/index.ts",
55
+ "types": "./dist/types/src/index.d.ts",
56
+ "import": "./dist/lib/index.mjs"
57
+ },
58
+ "./plugin": {
59
+ "source": "./src/plugin.ts",
60
+ "types": "./dist/types/src/plugin.d.ts",
61
+ "import": "./dist/lib/plugin.mjs"
62
+ },
63
+ "./testing": {
64
+ "source": {
65
+ "node": "./src/testing/node.ts",
66
+ "default": "./src/testing/index.ts"
67
+ },
68
+ "types": "./dist/types/src/testing/index.d.ts",
69
+ "node": "./dist/lib/testing/node.mjs",
70
+ "default": "./dist/lib/testing.mjs"
71
+ },
72
+ "./translations": {
73
+ "source": "./src/translations.ts",
74
+ "types": "./dist/types/src/translations.d.ts",
75
+ "import": "./dist/lib/translations.mjs"
76
+ }
77
+ },
78
+ "types": "dist/types/src/index.d.ts",
79
+ "files": [
80
+ "dist",
81
+ "dx.config.ts",
82
+ "src"
83
+ ],
84
+ "dependencies": {
85
+ "date-fns": "^3.6.0",
86
+ "@dxos/app-framework": "0.11.1",
87
+ "@dxos/app-toolkit": "0.11.1",
88
+ "@dxos/effect": "0.11.1",
89
+ "@dxos/extractor-lib": "0.11.1",
90
+ "@dxos/compute-runtime": "0.11.1",
91
+ "@dxos/echo": "0.11.1",
92
+ "@dxos/keys": "0.11.1",
93
+ "@dxos/link": "0.11.1",
94
+ "@dxos/errors": "0.11.1",
95
+ "@dxos/extractor": "0.11.1",
96
+ "@dxos/log": "0.11.1",
97
+ "@dxos/plugin-inbox": "0.11.1",
98
+ "@dxos/pipeline": "0.11.1",
99
+ "@dxos/types": "0.11.1",
100
+ "@dxos/util": "0.11.1",
101
+ "@dxos/plugin-connector": "0.11.1",
102
+ "@dxos/pipeline-email": "0.11.1",
103
+ "@dxos/compute": "0.11.1",
104
+ "@dxos/schema": "0.11.1"
105
+ },
106
+ "devDependencies": {
107
+ "@effect/platform": "0.96.2",
108
+ "@effect/vitest": "0.29.0",
109
+ "effect": "3.21.4",
110
+ "vite": "^8.1.4",
111
+ "@dxos/random": "0.11.1",
112
+ "@dxos/echo-client": "0.11.1"
113
+ },
114
+ "peerDependencies": {
115
+ "@effect/platform": "0.96.2",
116
+ "effect": "3.21.4"
117
+ },
118
+ "publishConfig": {
119
+ "access": "public"
120
+ }
121
+ }
@@ -0,0 +1,25 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import * as Plugin from '@dxos/app-framework/Plugin';
6
+ import * as AppCapability from '@dxos/app-toolkit/AppCapability';
7
+
8
+ import { Connector, MailSend, OperationHandler } from '#capabilities';
9
+ import { meta } from '#meta';
10
+ import { translations } from '#translations';
11
+
12
+ /**
13
+ * Headless mail provider: contributes the JMAP connector, its send-routing entry, and the sync/send/
14
+ * materialize handlers. Every UI surface, the `Mailbox` type, and the sync harness these handlers run
15
+ * against belong to `@dxos/plugin-inbox`.
16
+ */
17
+ export const JmapPlugin = Plugin.define(meta).pipe(
18
+ Plugin.addModule(OperationHandler),
19
+ Plugin.addModule(AppCapability.translations(translations)),
20
+ Plugin.addModule(Connector),
21
+ Plugin.addModule(MailSend),
22
+ Plugin.make,
23
+ );
24
+
25
+ export default JmapPlugin;
@@ -0,0 +1,138 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import * as HttpClient from '@effect/platform/HttpClient';
6
+ import * as HttpClientRequest from '@effect/platform/HttpClientRequest';
7
+ import * as HttpClientResponse from '@effect/platform/HttpClientResponse';
8
+ import * as Effect from 'effect/Effect';
9
+ import * as Schedule from 'effect/Schedule';
10
+ import * as Schema from 'effect/Schema';
11
+
12
+ import { withAuthorization } from '@dxos/compute-runtime';
13
+
14
+ import { JmapApiError } from '../../errors';
15
+ import { JmapCredentials } from '../../services/jmap-credentials';
16
+ import { MethodError, Response, Session } from './types';
17
+
18
+ /** Shared per-attempt timeout/retry policy for JMAP HTTP requests (session, batched API, blob download). */
19
+ export const REQUEST_TIMEOUT = '10 seconds';
20
+ export const REQUEST_RETRY = Schedule.exponential(1_000).pipe(Schedule.compose(Schedule.recurs(3)));
21
+
22
+ // Retry transient failures (network, 5xx, timeout) but not 4xx — a bad host/token or malformed
23
+ // request will fail identically on retry, so retrying only adds latency.
24
+ export const shouldRetry = (error: unknown): boolean =>
25
+ !(error instanceof JmapApiError && error.status !== undefined && error.status >= 400 && error.status < 500);
26
+
27
+ /** Capability set for read/write mail requests. */
28
+ export const MAIL_CAPABILITIES = ['urn:ietf:params:jmap:core', 'urn:ietf:params:jmap:mail'] as const;
29
+ /** Capability set including submission, required to send mail. */
30
+ export const SUBMISSION_CAPABILITIES = [...MAIL_CAPABILITIES, 'urn:ietf:params:jmap:submission'] as const;
31
+
32
+ /** A single `[methodName, args, callId]` invocation in a batched request. */
33
+ export type MethodCall = readonly [string, unknown, string];
34
+
35
+ /** A batched JMAP API request body (RFC 8620 §3.3). */
36
+ export type RequestBody = {
37
+ readonly using: readonly string[];
38
+ readonly methodCalls: readonly MethodCall[];
39
+ };
40
+
41
+ /**
42
+ * Discovers the JMAP session: `GET https://${host}/.well-known/jmap` (the well-known URL
43
+ * 302-redirects to the session resource, which `fetch` follows). Yields `apiUrl` +
44
+ * `primaryAccounts`. Tracer disabled (Effect + CORS: https://github.com/Effect-TS/effect/issues/4568).
45
+ */
46
+ export const getSession: Effect.Effect<Session, JmapApiError, HttpClient.HttpClient | JmapCredentials> = Effect.gen(
47
+ function* () {
48
+ const { host, token } = yield* JmapCredentials;
49
+ const httpClient = yield* HttpClient.HttpClient.pipe(Effect.map(withAuthorization(token, 'Bearer')));
50
+ const httpClientWithTracerDisabled = httpClient.pipe(HttpClient.withTracerDisabledWhen(() => true));
51
+
52
+ return yield* HttpClientRequest.get(`https://${host}/.well-known/jmap`).pipe(
53
+ HttpClientRequest.setHeader('accept', 'application/json'),
54
+ httpClientWithTracerDisabled.execute,
55
+ Effect.flatMap(decodeJsonOrFail(Session)),
56
+ Effect.timeout(REQUEST_TIMEOUT),
57
+ Effect.retry({ schedule: REQUEST_RETRY, while: shouldRetry }),
58
+ Effect.scoped,
59
+ Effect.withSpan('JmapGetSession'),
60
+ Effect.mapError(asJmapApiError),
61
+ );
62
+ },
63
+ );
64
+
65
+ /**
66
+ * Issues a batched JMAP API request (single POST to the session `apiUrl`) and decodes the response
67
+ * envelope. Read requests retry on transient failure; pass `{ retry: false }` for non-idempotent
68
+ * writes (draft create + submission) so a retry can't double-send.
69
+ */
70
+ export const jmapRequest = (
71
+ apiUrl: string,
72
+ body: RequestBody,
73
+ options: { retry?: boolean } = {},
74
+ ): Effect.Effect<Response, JmapApiError, HttpClient.HttpClient | JmapCredentials> =>
75
+ Effect.gen(function* () {
76
+ const { token } = yield* JmapCredentials;
77
+ const httpClient = yield* HttpClient.HttpClient.pipe(Effect.map(withAuthorization(token, 'Bearer')));
78
+ const httpClientWithTracerDisabled = httpClient.pipe(HttpClient.withTracerDisabledWhen(() => true));
79
+
80
+ const executed = HttpClientRequest.post(apiUrl).pipe(
81
+ HttpClientRequest.setHeader('accept', 'application/json'),
82
+ HttpClientRequest.bodyUnsafeJson(body),
83
+ httpClientWithTracerDisabled.execute,
84
+ Effect.flatMap(decodeJsonOrFail(Response)),
85
+ Effect.timeout(REQUEST_TIMEOUT),
86
+ );
87
+
88
+ return yield* (
89
+ options.retry === false ? executed : executed.pipe(Effect.retry({ schedule: REQUEST_RETRY, while: shouldRetry }))
90
+ ).pipe(Effect.scoped, Effect.withSpan('JmapApiRequest'), Effect.mapError(asJmapApiError));
91
+ });
92
+
93
+ /**
94
+ * Extracts and decodes the result for a given `callId` from a response envelope. A method-level
95
+ * `["error", …]` response (or a decode failure) becomes a typed {@link JmapApiError}.
96
+ */
97
+ export const getMethodResponse = <A, I, R>(
98
+ response: Response,
99
+ callId: string,
100
+ schema: Schema.Schema<A, I, R>,
101
+ ): Effect.Effect<A, JmapApiError, R> => {
102
+ const entry = response.methodResponses.find((methodResponse) => methodResponse[2] === callId);
103
+ if (!entry) {
104
+ return Effect.fail(new JmapApiError(undefined, `No method response for call ${callId}.`));
105
+ }
106
+
107
+ const [name, args] = entry;
108
+ if (name === 'error') {
109
+ return Schema.decodeUnknown(MethodError)(args).pipe(
110
+ Effect.matchEffect({
111
+ onFailure: () => Effect.fail(new JmapApiError(undefined, `JMAP method error (${callId}).`)),
112
+ onSuccess: (error) => Effect.fail(new JmapApiError(undefined, error.description ?? error.type, error.type)),
113
+ }),
114
+ );
115
+ }
116
+
117
+ return Schema.decodeUnknown(schema)(args).pipe(Effect.mapError(asJmapApiError));
118
+ };
119
+
120
+ /** Decodes a 2xx JSON body against `schema`; a >=400 status fails with the (truncated) body text. */
121
+ const decodeJsonOrFail =
122
+ <A, I, R>(schema: Schema.Schema<A, I, R>) =>
123
+ (response: HttpClientResponse.HttpClientResponse): Effect.Effect<A, JmapApiError, R> =>
124
+ Effect.gen(function* () {
125
+ if (response.status >= 400) {
126
+ const body = yield* response.text;
127
+ return yield* Effect.fail(new JmapApiError(response.status, truncate(body)));
128
+ }
129
+ return yield* HttpClientResponse.schemaBodyJson(schema)(response);
130
+ }).pipe(Effect.mapError(asJmapApiError));
131
+
132
+ /** Collapses transport/decode failures (HttpClientError, ParseError, timeout) into a typed JmapApiError. */
133
+ const asJmapApiError = (error: unknown): JmapApiError =>
134
+ error instanceof JmapApiError
135
+ ? error
136
+ : new JmapApiError(undefined, error instanceof Error ? error.message : String(error));
137
+
138
+ const truncate = (text: string, max = 500): string => (text.length > max ? `${text.slice(0, max)}…` : text);
@@ -0,0 +1,6 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ export * from './api';
6
+ export * from './types';
@@ -0,0 +1,86 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import * as Schema from 'effect/Schema';
6
+
7
+ //
8
+ // Shared JMAP core schemas (RFC 8620). These are capability-agnostic — they apply to mail, calendars,
9
+ // and contacts alike.
10
+ //
11
+
12
+ /** Session object returned from `https://${host}/.well-known/jmap` (RFC 8620 §2). */
13
+ export const Session = Schema.Struct({
14
+ /** Endpoint for all subsequent API requests. */
15
+ apiUrl: Schema.String,
16
+ /**
17
+ * URI Template (RFC 6570) for blob downloads, with `accountId`/`blobId`/`type`/`name` variables
18
+ * (RFC 8620 §6.2). Required, matching the spec — every real JMAP session advertises one.
19
+ */
20
+ downloadUrl: Schema.String,
21
+ /** Authenticated username (used to seed the connection account when the form leaves it blank). */
22
+ username: Schema.optional(Schema.String),
23
+ /** Capability urn → primary account id (e.g. `urn:ietf:params:jmap:mail`). */
24
+ primaryAccounts: Schema.Record({ key: Schema.String, value: Schema.String }),
25
+ accounts: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.Unknown })),
26
+ capabilities: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.Unknown })),
27
+ state: Schema.optional(Schema.String),
28
+ });
29
+ export type Session = Schema.Schema.Type<typeof Session>;
30
+
31
+ /** A method-level error response: `["error", { type, description }, callId]` (RFC 8620 §3.6.1). */
32
+ export const MethodError = Schema.Struct({
33
+ type: Schema.String,
34
+ description: Schema.optional(Schema.NullOr(Schema.String)),
35
+ });
36
+ export type MethodError = Schema.Schema.Type<typeof MethodError>;
37
+
38
+ /** A single `[name, args, callId]` invocation result; `args` is decoded per-method by the caller. */
39
+ export const MethodResponse = Schema.Tuple(Schema.String, Schema.Unknown, Schema.String);
40
+ export type MethodResponse = Schema.Schema.Type<typeof MethodResponse>;
41
+
42
+ /** The top-level response envelope of a batched API request (RFC 8620 §3.3). */
43
+ export const Response = Schema.Struct({
44
+ methodResponses: Schema.Array(MethodResponse),
45
+ sessionState: Schema.optional(Schema.String),
46
+ });
47
+ export type Response = Schema.Schema.Type<typeof Response>;
48
+
49
+ //
50
+ // `Email/query` filter (RFC 8621 §4.4.1). Constructed client-side (never decoded), so these are plain
51
+ // types rather than Schemas. Shared here because future calendar/contacts filters share the same
52
+ // AND/OR/NOT operator structure (RFC 8620 §5.5).
53
+ //
54
+
55
+ /** A single filter condition; all present properties must match. */
56
+ export type FilterCondition = {
57
+ readonly inMailbox?: string;
58
+ readonly inMailboxOtherThan?: readonly string[];
59
+ readonly before?: string;
60
+ readonly after?: string;
61
+ readonly minSize?: number;
62
+ readonly maxSize?: number;
63
+ readonly hasKeyword?: string;
64
+ readonly notKeyword?: string;
65
+ readonly allInThreadHaveKeyword?: string;
66
+ readonly someInThreadHaveKeyword?: string;
67
+ readonly noneInThreadHaveKeyword?: string;
68
+ readonly text?: string;
69
+ readonly from?: string;
70
+ readonly to?: string;
71
+ readonly cc?: string;
72
+ readonly bcc?: string;
73
+ readonly subject?: string;
74
+ readonly body?: string;
75
+ readonly header?: readonly string[];
76
+ readonly hasAttachment?: boolean;
77
+ };
78
+
79
+ /** Combines sub-filters (RFC 8620 §5.5). */
80
+ export type FilterOperator = {
81
+ readonly operator: 'AND' | 'OR' | 'NOT';
82
+ readonly conditions: readonly Filter[];
83
+ };
84
+
85
+ /** An `Email/query` filter: a single condition or an AND/OR/NOT combination. */
86
+ export type Filter = FilterCondition | FilterOperator;