@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.
- package/LICENSE +105 -0
- package/dist/lib/JmapPlugin.mjs +16 -0
- package/dist/lib/JmapPlugin.mjs.map +1 -0
- package/dist/lib/apis.mjs +2 -0
- package/dist/lib/capabilities.mjs +17 -0
- package/dist/lib/capabilities.mjs.map +1 -0
- package/dist/lib/chunk-apis.mjs +756 -0
- package/dist/lib/chunk-apis.mjs.map +1 -0
- package/dist/lib/chunk-connector.mjs +111 -0
- package/dist/lib/chunk-connector.mjs.map +1 -0
- package/dist/lib/chunk-constants.mjs +23 -0
- package/dist/lib/chunk-constants.mjs.map +1 -0
- package/dist/lib/chunk-errors.mjs +42 -0
- package/dist/lib/chunk-errors.mjs.map +1 -0
- package/dist/lib/chunk-handler.mjs +27 -0
- package/dist/lib/chunk-handler.mjs.map +1 -0
- package/dist/lib/chunk-jmap-fixtures.mjs +102 -0
- package/dist/lib/chunk-jmap-fixtures.mjs.map +1 -0
- package/dist/lib/chunk-mail-send.mjs +17 -0
- package/dist/lib/chunk-mail-send.mjs.map +1 -0
- package/dist/lib/chunk-meta.mjs +37 -0
- package/dist/lib/chunk-meta.mjs.map +1 -0
- package/dist/lib/chunk-operation-handler.mjs +12 -0
- package/dist/lib/chunk-operation-handler.mjs.map +1 -0
- package/dist/lib/chunk-send.mjs +98 -0
- package/dist/lib/chunk-send.mjs.map +1 -0
- package/dist/lib/chunk-sync-provider.mjs +511 -0
- package/dist/lib/chunk-sync-provider.mjs.map +1 -0
- package/dist/lib/chunk-sync.mjs +37 -0
- package/dist/lib/chunk-sync.mjs.map +1 -0
- package/dist/lib/index.mjs +4 -0
- package/dist/lib/meta.mjs +2 -0
- package/dist/lib/operations.mjs +13 -0
- package/dist/lib/operations.mjs.map +1 -0
- package/dist/lib/plugin.mjs +9 -0
- package/dist/lib/plugin.mjs.map +1 -0
- package/dist/lib/services.mjs +139 -0
- package/dist/lib/services.mjs.map +1 -0
- package/dist/lib/testing/node.mjs +2 -0
- package/dist/lib/testing.mjs +19 -0
- package/dist/lib/testing.mjs.map +1 -0
- package/dist/lib/translations.mjs +7 -0
- package/dist/lib/translations.mjs.map +1 -0
- package/dist/types/dx.config.d.ts +28 -0
- package/dist/types/dx.config.d.ts.map +1 -0
- package/dist/types/src/JmapPlugin.d.ts +9 -0
- package/dist/types/src/JmapPlugin.d.ts.map +1 -0
- package/dist/types/src/apis/Jmap/api.d.ts +42 -0
- package/dist/types/src/apis/Jmap/api.d.ts.map +1 -0
- package/dist/types/src/apis/Jmap/index.d.ts +3 -0
- package/dist/types/src/apis/Jmap/index.d.ts.map +1 -0
- package/dist/types/src/apis/Jmap/types.d.ts +65 -0
- package/dist/types/src/apis/Jmap/types.d.ts.map +1 -0
- package/dist/types/src/apis/JmapMail/api.d.ts +223 -0
- package/dist/types/src/apis/JmapMail/api.d.ts.map +1 -0
- package/dist/types/src/apis/JmapMail/index.d.ts +4 -0
- package/dist/types/src/apis/JmapMail/index.d.ts.map +1 -0
- package/dist/types/src/apis/JmapMail/query.d.ts +26 -0
- package/dist/types/src/apis/JmapMail/query.d.ts.map +1 -0
- package/dist/types/src/apis/JmapMail/query.test.d.ts +2 -0
- package/dist/types/src/apis/JmapMail/query.test.d.ts.map +1 -0
- package/dist/types/src/apis/JmapMail/types.d.ts +290 -0
- package/dist/types/src/apis/JmapMail/types.d.ts.map +1 -0
- package/dist/types/src/apis/index.d.ts +3 -0
- package/dist/types/src/apis/index.d.ts.map +1 -0
- package/dist/types/src/apis/jmap-api.test.d.ts +2 -0
- package/dist/types/src/apis/jmap-api.test.d.ts.map +1 -0
- package/dist/types/src/capabilities/connector.d.ts +5 -0
- package/dist/types/src/capabilities/connector.d.ts.map +1 -0
- package/dist/types/src/capabilities/credential-form.d.ts +37 -0
- package/dist/types/src/capabilities/credential-form.d.ts.map +1 -0
- package/dist/types/src/capabilities/credential-form.test.d.ts +2 -0
- package/dist/types/src/capabilities/credential-form.test.d.ts.map +1 -0
- package/dist/types/src/capabilities/index.d.ts +5 -0
- package/dist/types/src/capabilities/index.d.ts.map +1 -0
- package/dist/types/src/capabilities/mail-send.d.ts +6 -0
- package/dist/types/src/capabilities/mail-send.d.ts.map +1 -0
- package/dist/types/src/capabilities/operation-handler.d.ts +5 -0
- package/dist/types/src/capabilities/operation-handler.d.ts.map +1 -0
- package/dist/types/src/constants.d.ts +19 -0
- package/dist/types/src/constants.d.ts.map +1 -0
- package/dist/types/src/errors.d.ts +103 -0
- package/dist/types/src/errors.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +4 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/src/meta.d.ts +33 -0
- package/dist/types/src/meta.d.ts.map +1 -0
- package/dist/types/src/operations/index.d.ts +3 -0
- package/dist/types/src/operations/index.d.ts.map +1 -0
- package/dist/types/src/operations/mail/mapper.d.ts +49 -0
- package/dist/types/src/operations/mail/mapper.d.ts.map +1 -0
- package/dist/types/src/operations/mail/materialize/handler.d.ts +11 -0
- package/dist/types/src/operations/mail/materialize/handler.d.ts.map +1 -0
- package/dist/types/src/operations/mail/send/handler.d.ts +4 -0
- package/dist/types/src/operations/mail/send/handler.d.ts.map +1 -0
- package/dist/types/src/operations/mail/send/index.d.ts +2 -0
- package/dist/types/src/operations/mail/send/index.d.ts.map +1 -0
- package/dist/types/src/operations/mail/sync/handler.d.ts +4 -0
- package/dist/types/src/operations/mail/sync/handler.d.ts.map +1 -0
- package/dist/types/src/operations/mail/sync/index.d.ts +3 -0
- package/dist/types/src/operations/mail/sync/index.d.ts.map +1 -0
- package/dist/types/src/operations/mail/sync/mapper.test.d.ts +2 -0
- package/dist/types/src/operations/mail/sync/mapper.test.d.ts.map +1 -0
- package/dist/types/src/operations/mail/sync/sync-e2e.test.d.ts +2 -0
- package/dist/types/src/operations/mail/sync/sync-e2e.test.d.ts.map +1 -0
- package/dist/types/src/operations/mail/sync/sync-provider.d.ts +11 -0
- package/dist/types/src/operations/mail/sync/sync-provider.d.ts.map +1 -0
- package/dist/types/src/operations/mail/sync/sync.test.d.ts +2 -0
- package/dist/types/src/operations/mail/sync/sync.test.d.ts.map +1 -0
- package/dist/types/src/operations/mail/sync/system-tags.d.ts +14 -0
- package/dist/types/src/operations/mail/sync/system-tags.d.ts.map +1 -0
- package/dist/types/src/operations/mail/tags.d.ts +12 -0
- package/dist/types/src/operations/mail/tags.d.ts.map +1 -0
- package/dist/types/src/plugin.d.ts +4 -0
- package/dist/types/src/plugin.d.ts.map +1 -0
- package/dist/types/src/services/index.d.ts +3 -0
- package/dist/types/src/services/index.d.ts.map +1 -0
- package/dist/types/src/services/jmap-credentials.d.ts +39 -0
- package/dist/types/src/services/jmap-credentials.d.ts.map +1 -0
- package/dist/types/src/services/jmap-mail-api.d.ts +109 -0
- package/dist/types/src/services/jmap-mail-api.d.ts.map +1 -0
- package/dist/types/src/testing/index.d.ts +5 -0
- package/dist/types/src/testing/index.d.ts.map +1 -0
- package/dist/types/src/testing/jmap-fixtures.d.ts +26 -0
- package/dist/types/src/testing/jmap-fixtures.d.ts.map +1 -0
- package/dist/types/src/testing/jmap-fixtures.test.d.ts +2 -0
- package/dist/types/src/testing/jmap-fixtures.test.d.ts.map +1 -0
- package/dist/types/src/testing/node.d.ts +4 -0
- package/dist/types/src/testing/node.d.ts.map +1 -0
- package/dist/types/src/testing/sync-fixture.d.ts +22 -0
- package/dist/types/src/testing/sync-fixture.d.ts.map +1 -0
- package/dist/types/src/translations.d.ts +8 -0
- package/dist/types/src/translations.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/dx.config.ts +32 -0
- package/package.json +121 -0
- package/src/JmapPlugin.ts +25 -0
- package/src/apis/Jmap/api.ts +138 -0
- package/src/apis/Jmap/index.ts +6 -0
- package/src/apis/Jmap/types.ts +86 -0
- package/src/apis/JmapMail/api.ts +324 -0
- package/src/apis/JmapMail/index.ts +7 -0
- package/src/apis/JmapMail/query.test.ts +181 -0
- package/src/apis/JmapMail/query.ts +231 -0
- package/src/apis/JmapMail/types.ts +196 -0
- package/src/apis/index.ts +8 -0
- package/src/apis/jmap-api.test.ts +261 -0
- package/src/capabilities/connector.ts +44 -0
- package/src/capabilities/credential-form.test.ts +69 -0
- package/src/capabilities/credential-form.ts +113 -0
- package/src/capabilities/index.ts +24 -0
- package/src/capabilities/mail-send.ts +21 -0
- package/src/capabilities/operation-handler.ts +16 -0
- package/src/constants.ts +24 -0
- package/src/errors.ts +38 -0
- package/src/index.ts +7 -0
- package/src/meta.ts +9 -0
- package/src/operations/index.ts +13 -0
- package/src/operations/mail/mapper.ts +191 -0
- package/src/operations/mail/materialize/handler.ts +41 -0
- package/src/operations/mail/send/handler.ts +116 -0
- package/src/operations/mail/send/index.ts +5 -0
- package/src/operations/mail/sync/handler.ts +52 -0
- package/src/operations/mail/sync/index.ts +7 -0
- package/src/operations/mail/sync/mapper.test.ts +150 -0
- package/src/operations/mail/sync/sync-e2e.test.ts +63 -0
- package/src/operations/mail/sync/sync-provider.ts +481 -0
- package/src/operations/mail/sync/sync.test.ts +766 -0
- package/src/operations/mail/sync/system-tags.ts +24 -0
- package/src/operations/mail/tags.ts +29 -0
- package/src/plugin.ts +11 -0
- package/src/services/index.ts +6 -0
- package/src/services/jmap-credentials.ts +61 -0
- package/src/services/jmap-mail-api.ts +294 -0
- package/src/testing/index.ts +8 -0
- package/src/testing/jmap-fixtures.test.ts +107 -0
- package/src/testing/jmap-fixtures.ts +105 -0
- package/src/testing/node.ts +12 -0
- package/src/testing/sync-fixture.ts +39 -0
- package/src/translations.ts +15 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as FetchHttpClient from '@effect/platform/FetchHttpClient';
|
|
6
|
+
import { describe, it } from '@effect/vitest';
|
|
7
|
+
import * as Effect from 'effect/Effect';
|
|
8
|
+
import * as Layer from 'effect/Layer';
|
|
9
|
+
import { afterEach, beforeEach, vi } from 'vitest';
|
|
10
|
+
|
|
11
|
+
import { JmapApiError } from '../errors';
|
|
12
|
+
import { JmapCredentials } from '../services';
|
|
13
|
+
import { Jmap, JmapMail } from './index';
|
|
14
|
+
|
|
15
|
+
const HOST = 'api.fastmail.com';
|
|
16
|
+
const ACCOUNT_ID = 'u9999';
|
|
17
|
+
const API_URL = 'https://api.fastmail.com/jmap/api/';
|
|
18
|
+
|
|
19
|
+
const SESSION = {
|
|
20
|
+
apiUrl: API_URL,
|
|
21
|
+
downloadUrl: 'https://api.fastmail.com/jmap/download/{accountId}/{blobId}/{name}?type={type}',
|
|
22
|
+
username: 'alice@fastmail.com',
|
|
23
|
+
primaryAccounts: {
|
|
24
|
+
'urn:ietf:params:jmap:mail': ACCOUNT_ID,
|
|
25
|
+
'urn:ietf:params:jmap:submission': ACCOUNT_ID,
|
|
26
|
+
},
|
|
27
|
+
accounts: { [ACCOUNT_ID]: { name: 'alice@fastmail.com', isPersonal: true } },
|
|
28
|
+
capabilities: { 'urn:ietf:params:jmap:core': {}, 'urn:ietf:params:jmap:mail': {} },
|
|
29
|
+
state: 'cyrus-0',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const TARGET: JmapMail.Target = { apiUrl: API_URL, accountId: ACCOUNT_ID };
|
|
33
|
+
|
|
34
|
+
const TestLayer = Layer.mergeAll(
|
|
35
|
+
FetchHttpClient.layer,
|
|
36
|
+
JmapCredentials.fromValues({ host: HOST, token: 'test-token' }),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
type MockRequest = { url: string; method: string; body: any };
|
|
40
|
+
type MockResult = { status?: number; body: unknown };
|
|
41
|
+
|
|
42
|
+
let respond: (request: MockRequest) => MockResult;
|
|
43
|
+
let originalFetch: typeof globalThis.fetch;
|
|
44
|
+
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
originalFetch = globalThis.fetch;
|
|
47
|
+
globalThis.fetch = vi.fn(async (...args: Parameters<typeof fetch>): Promise<Response> => {
|
|
48
|
+
const [input, init] = args;
|
|
49
|
+
const request = input instanceof Request ? input : new Request(input, init);
|
|
50
|
+
const text = request.method === 'POST' ? await request.text() : '';
|
|
51
|
+
const result = respond({ url: request.url, method: request.method, body: text ? JSON.parse(text) : undefined });
|
|
52
|
+
return new Response(JSON.stringify(result.body ?? {}), {
|
|
53
|
+
status: result.status ?? 200,
|
|
54
|
+
headers: { 'content-type': 'application/json' },
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
globalThis.fetch = originalFetch;
|
|
61
|
+
vi.restoreAllMocks();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('JMAP API', () => {
|
|
65
|
+
it.effect(
|
|
66
|
+
'getSession discovers the well-known URL and returns apiUrl + account',
|
|
67
|
+
Effect.fnUntraced(function* ({ expect }) {
|
|
68
|
+
respond = ({ url, method }) => {
|
|
69
|
+
expect(method).toBe('GET');
|
|
70
|
+
expect(url).toContain('/.well-known/jmap');
|
|
71
|
+
return { body: SESSION };
|
|
72
|
+
};
|
|
73
|
+
const session = yield* Jmap.getSession;
|
|
74
|
+
expect(session.apiUrl).toBe(API_URL);
|
|
75
|
+
expect(session.primaryAccounts['urn:ietf:params:jmap:mail']).toBe(ACCOUNT_ID);
|
|
76
|
+
expect(session.username).toBe('alice@fastmail.com');
|
|
77
|
+
}, Effect.provide(TestLayer)),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
it.effect(
|
|
81
|
+
'mailboxGet returns folders with roles',
|
|
82
|
+
Effect.fnUntraced(function* ({ expect }) {
|
|
83
|
+
respond = ({ body }) => {
|
|
84
|
+
expect(body.methodCalls[0][0]).toBe('Mailbox/get');
|
|
85
|
+
return {
|
|
86
|
+
body: {
|
|
87
|
+
methodResponses: [
|
|
88
|
+
[
|
|
89
|
+
'Mailbox/get',
|
|
90
|
+
{
|
|
91
|
+
accountId: ACCOUNT_ID,
|
|
92
|
+
list: [
|
|
93
|
+
{ id: 'mb-inbox', name: 'Inbox', role: 'inbox' },
|
|
94
|
+
{ id: 'mb-sent', name: 'Sent', role: 'sent' },
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
'0',
|
|
98
|
+
],
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
const result = yield* JmapMail.mailboxGet(TARGET);
|
|
104
|
+
expect(result.list).toHaveLength(2);
|
|
105
|
+
expect(result.list.find((folder) => folder.role === 'inbox')?.id).toBe('mb-inbox');
|
|
106
|
+
}, Effect.provide(TestLayer)),
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
it.effect(
|
|
110
|
+
'emailQuery sends the filter + sort and returns ids',
|
|
111
|
+
Effect.fnUntraced(function* ({ expect }) {
|
|
112
|
+
respond = ({ body }) => {
|
|
113
|
+
const [name, args] = body.methodCalls[0];
|
|
114
|
+
expect(name).toBe('Email/query');
|
|
115
|
+
expect(args.accountId).toBe(ACCOUNT_ID);
|
|
116
|
+
expect(args.filter).toEqual({ inMailbox: 'mb-inbox' });
|
|
117
|
+
expect(args.sort).toEqual([{ property: 'receivedAt', isAscending: false }]);
|
|
118
|
+
expect(args.limit).toBe(50);
|
|
119
|
+
return {
|
|
120
|
+
body: { methodResponses: [['Email/query', { accountId: ACCOUNT_ID, ids: ['e1', 'e2'], total: 2 }, '0']] },
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
const result = yield* JmapMail.emailQuery(TARGET, {
|
|
124
|
+
filter: { inMailbox: 'mb-inbox' },
|
|
125
|
+
sort: [{ property: 'receivedAt', isAscending: false }],
|
|
126
|
+
limit: 50,
|
|
127
|
+
});
|
|
128
|
+
expect(result.ids).toEqual(['e1', 'e2']);
|
|
129
|
+
}, Effect.provide(TestLayer)),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
it.effect(
|
|
133
|
+
'emailGet requests html + text body values and decodes emails',
|
|
134
|
+
Effect.fnUntraced(function* ({ expect }) {
|
|
135
|
+
respond = ({ body }) => {
|
|
136
|
+
const [name, args] = body.methodCalls[0];
|
|
137
|
+
expect(name).toBe('Email/get');
|
|
138
|
+
expect(args.ids).toEqual(['e1']);
|
|
139
|
+
expect(args.fetchTextBodyValues).toBe(true);
|
|
140
|
+
expect(args.fetchHTMLBodyValues).toBe(true);
|
|
141
|
+
return {
|
|
142
|
+
body: {
|
|
143
|
+
methodResponses: [
|
|
144
|
+
[
|
|
145
|
+
'Email/get',
|
|
146
|
+
{
|
|
147
|
+
accountId: ACCOUNT_ID,
|
|
148
|
+
list: [
|
|
149
|
+
{
|
|
150
|
+
id: 'e1',
|
|
151
|
+
threadId: 't-e1',
|
|
152
|
+
receivedAt: '2026-01-01T00:00:00.000Z',
|
|
153
|
+
from: [{ name: 'A', email: 'a@b.com' }],
|
|
154
|
+
bodyValues: { body: { value: 'hi' } },
|
|
155
|
+
textBody: [{ partId: 'body' }],
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
},
|
|
159
|
+
'0',
|
|
160
|
+
],
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
const result = yield* JmapMail.emailGet(TARGET, ['e1']);
|
|
166
|
+
expect(result.list[0].id).toBe('e1');
|
|
167
|
+
expect(result.list[0].bodyValues?.body?.value).toBe('hi');
|
|
168
|
+
}, Effect.provide(TestLayer)),
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
it.effect(
|
|
172
|
+
'submitEmail creates the draft and submission in a single batched request',
|
|
173
|
+
Effect.fnUntraced(function* ({ expect }) {
|
|
174
|
+
respond = ({ body }) => {
|
|
175
|
+
expect(body.using).toContain('urn:ietf:params:jmap:submission');
|
|
176
|
+
const [setName, setArgs, setId] = body.methodCalls[0];
|
|
177
|
+
const [subName, subArgs, subId] = body.methodCalls[1];
|
|
178
|
+
expect(setName).toBe('Email/set');
|
|
179
|
+
expect(setArgs.create.draft.mailboxIds).toEqual({ 'mb-drafts': true });
|
|
180
|
+
expect(setArgs.create.draft.keywords).toEqual({ $draft: true, $seen: true });
|
|
181
|
+
expect(subName).toBe('EmailSubmission/set');
|
|
182
|
+
expect(subArgs.create.sub.emailId).toBe('#draft');
|
|
183
|
+
expect(subArgs.onSuccessUpdateEmail['#sub']['mailboxIds/mb-sent']).toBe(true);
|
|
184
|
+
expect(subArgs.onSuccessUpdateEmail['#sub']['mailboxIds/mb-drafts']).toBeNull();
|
|
185
|
+
return {
|
|
186
|
+
body: {
|
|
187
|
+
methodResponses: [
|
|
188
|
+
[setName, { accountId: ACCOUNT_ID, created: { draft: { id: 'new-1', threadId: 't-1' } } }, setId],
|
|
189
|
+
[subName, { accountId: ACCOUNT_ID, created: { sub: { id: 'sub-1' } } }, subId],
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
};
|
|
194
|
+
const result = yield* JmapMail.submitEmail(TARGET, {
|
|
195
|
+
identityId: 'id-1',
|
|
196
|
+
draftsMailboxId: 'mb-drafts',
|
|
197
|
+
sentMailboxId: 'mb-sent',
|
|
198
|
+
draft: { from: [{ email: 'me@x.com' }], to: [{ email: 'you@x.com' }], subject: 'Hi', text: 'Body' },
|
|
199
|
+
});
|
|
200
|
+
expect(result.id).toBe('new-1');
|
|
201
|
+
expect(result.threadId).toBe('t-1');
|
|
202
|
+
}, Effect.provide(TestLayer)),
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
it.effect(
|
|
206
|
+
'identityGet lists sending identities',
|
|
207
|
+
Effect.fnUntraced(function* ({ expect }) {
|
|
208
|
+
respond = ({ body }) => {
|
|
209
|
+
const [name, args] = body.methodCalls[0];
|
|
210
|
+
expect(name).toBe('Identity/get');
|
|
211
|
+
expect(body.using).toContain('urn:ietf:params:jmap:submission');
|
|
212
|
+
expect(args.accountId).toBe(ACCOUNT_ID);
|
|
213
|
+
return {
|
|
214
|
+
body: {
|
|
215
|
+
methodResponses: [
|
|
216
|
+
['Identity/get', { accountId: ACCOUNT_ID, list: [{ id: 'id-1', email: 'me@x.com' }] }, '0'],
|
|
217
|
+
],
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
};
|
|
221
|
+
const result = yield* JmapMail.identityGet(TARGET);
|
|
222
|
+
expect(result.list[0].email).toBe('me@x.com');
|
|
223
|
+
}, Effect.provide(TestLayer)),
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
it.effect(
|
|
227
|
+
'emailSetUpdate patches an email (move to trash)',
|
|
228
|
+
Effect.fnUntraced(function* ({ expect }) {
|
|
229
|
+
respond = ({ body }) => {
|
|
230
|
+
const [name, args] = body.methodCalls[0];
|
|
231
|
+
expect(name).toBe('Email/set');
|
|
232
|
+
expect(args.update).toEqual({ e1: { mailboxIds: { 'mb-trash': true } } });
|
|
233
|
+
return { body: { methodResponses: [['Email/set', { accountId: ACCOUNT_ID, updated: { e1: null } }, '0']] } };
|
|
234
|
+
};
|
|
235
|
+
const result = yield* JmapMail.emailSetUpdate(TARGET, 'e1', { mailboxIds: { 'mb-trash': true } });
|
|
236
|
+
expect(result.updated?.e1).toBeNull();
|
|
237
|
+
}, Effect.provide(TestLayer)),
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
it.effect(
|
|
241
|
+
'a 401 status surfaces as a JmapApiError carrying the status',
|
|
242
|
+
Effect.fnUntraced(function* ({ expect }) {
|
|
243
|
+
respond = () => ({ status: 401, body: { type: 'about:blank', status: 401 } });
|
|
244
|
+
const error = yield* Effect.flip(Jmap.getSession);
|
|
245
|
+
expect(error).toBeInstanceOf(JmapApiError);
|
|
246
|
+
expect(error.status).toBe(401);
|
|
247
|
+
}, Effect.provide(TestLayer)),
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
it.effect(
|
|
251
|
+
'a method-level error response surfaces as a JmapApiError carrying the type',
|
|
252
|
+
Effect.fnUntraced(function* ({ expect }) {
|
|
253
|
+
respond = () => ({
|
|
254
|
+
body: { methodResponses: [['error', { type: 'unknownMethod', description: 'nope' }, '0']] },
|
|
255
|
+
});
|
|
256
|
+
const error = yield* Effect.flip(JmapMail.mailboxGet(TARGET));
|
|
257
|
+
expect(error).toBeInstanceOf(JmapApiError);
|
|
258
|
+
expect(error.type).toBe('unknownMethod');
|
|
259
|
+
}, Effect.provide(TestLayer)),
|
|
260
|
+
);
|
|
261
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Effect from 'effect/Effect';
|
|
6
|
+
|
|
7
|
+
import * as Capability from '@dxos/app-framework/Capability';
|
|
8
|
+
import * as Trigger from '@dxos/compute/Trigger';
|
|
9
|
+
import { Type } from '@dxos/echo';
|
|
10
|
+
import * as ConnectorSpec from '@dxos/plugin-connector/ConnectorSpec';
|
|
11
|
+
import * as InboxOperation from '@dxos/plugin-inbox/InboxOperation';
|
|
12
|
+
import * as Mailbox from '@dxos/plugin-inbox/Mailbox';
|
|
13
|
+
import { MAIL_AUTO_SYNC, MAIL_REMOTE_SYNC, MAIL_SYNC_CRON } from '@dxos/plugin-inbox/sync';
|
|
14
|
+
import * as SyncOptions from '@dxos/plugin-inbox/SyncOptions';
|
|
15
|
+
|
|
16
|
+
import { JMAP_DEFAULT_HOST, JMAP_MAIL_CONNECTOR_ID } from '../constants';
|
|
17
|
+
import { jmapCredentialForm } from './credential-form';
|
|
18
|
+
|
|
19
|
+
export default Capability.makeModule(
|
|
20
|
+
Effect.fnUntraced(function* () {
|
|
21
|
+
return Capability.contribute(ConnectorSpec.Connector, [
|
|
22
|
+
{
|
|
23
|
+
id: JMAP_MAIL_CONNECTOR_ID,
|
|
24
|
+
// Nominal default; the real `AccessToken.source` (host) is captured by the credential form.
|
|
25
|
+
source: JMAP_DEFAULT_HOST,
|
|
26
|
+
label: 'JMAP Mail',
|
|
27
|
+
// Non-OAuth: host + email + Bearer API token, validated against the live session on submit.
|
|
28
|
+
credentialForm: jmapCredentialForm,
|
|
29
|
+
sync: {
|
|
30
|
+
operation: InboxOperation.JmapSync,
|
|
31
|
+
// What this connector binds — how `Mailbox` discovers it without naming JMAP.
|
|
32
|
+
targetTypename: Type.getTypename(Mailbox.Mailbox),
|
|
33
|
+
// Single-target connector (the account inbox): no `getTargets`. The coordinator calls
|
|
34
|
+
// `materializeTarget` (no remoteTarget) to create the Mailbox, then binds.
|
|
35
|
+
materializeTarget: InboxOperation.MaterializeJmapTarget,
|
|
36
|
+
optionsSchema: SyncOptions.SyncOptions,
|
|
37
|
+
auto: MAIL_AUTO_SYNC,
|
|
38
|
+
trigger: Trigger.specTimer(MAIL_SYNC_CRON),
|
|
39
|
+
remote: MAIL_REMOTE_SYNC,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
]);
|
|
43
|
+
}),
|
|
44
|
+
);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { describe, it, test } from '@effect/vitest';
|
|
6
|
+
import * as Effect from 'effect/Effect';
|
|
7
|
+
|
|
8
|
+
import { Jmap } from '../apis';
|
|
9
|
+
import { JMAP_DEFAULT_HOST } from '../constants';
|
|
10
|
+
import { buildJmapCredential, jmapCredentialForm } from './credential-form';
|
|
11
|
+
|
|
12
|
+
const session = (username?: string): Jmap.Session => ({
|
|
13
|
+
apiUrl: 'https://api.fastmail.com/jmap/api/',
|
|
14
|
+
downloadUrl: 'https://api.fastmail.com/jmap/download/{accountId}/{blobId}/{name}?type={type}',
|
|
15
|
+
primaryAccounts: { 'urn:ietf:params:jmap:mail': 'u1' },
|
|
16
|
+
username,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const connector = { id: 'jmap-mail', label: 'JMAP Mail' };
|
|
20
|
+
|
|
21
|
+
describe('jmapCredentialForm', () => {
|
|
22
|
+
test('defaults the host to the Fastmail endpoint', ({ expect }) => {
|
|
23
|
+
expect(jmapCredentialForm.defaultValues?.host).toBe(JMAP_DEFAULT_HOST);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('builds an AccessToken + Connection from the form email', ({ expect }) => {
|
|
27
|
+
const { accessToken, connection } = buildJmapCredential({
|
|
28
|
+
host: 'mail.example.com',
|
|
29
|
+
email: 'me@example.com',
|
|
30
|
+
token: 'secret',
|
|
31
|
+
session: session('login@example.com'),
|
|
32
|
+
connector,
|
|
33
|
+
});
|
|
34
|
+
expect(accessToken.source).toBe('mail.example.com');
|
|
35
|
+
expect(accessToken.account).toBe('me@example.com');
|
|
36
|
+
expect(accessToken.token).toBe('secret');
|
|
37
|
+
expect(connection.connectorId).toBe('jmap-mail');
|
|
38
|
+
expect(connection.name).toBe('JMAP Mail');
|
|
39
|
+
expect(connection.accessToken).toBeDefined();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('falls back to the session username when the email is blank', ({ expect }) => {
|
|
43
|
+
const { accessToken } = buildJmapCredential({
|
|
44
|
+
host: 'api.fastmail.com',
|
|
45
|
+
email: '',
|
|
46
|
+
token: 'secret',
|
|
47
|
+
session: session('login@fastmail.com'),
|
|
48
|
+
connector,
|
|
49
|
+
});
|
|
50
|
+
expect(accessToken.account).toBe('login@fastmail.com');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it.effect(
|
|
54
|
+
'onValidate rejects an empty token before any network call',
|
|
55
|
+
Effect.fnUntraced(function* ({ expect }) {
|
|
56
|
+
const { onValidate } = jmapCredentialForm;
|
|
57
|
+
if (!onValidate) {
|
|
58
|
+
throw new Error('expected an onValidate hook');
|
|
59
|
+
}
|
|
60
|
+
const error = yield* Effect.flip(
|
|
61
|
+
onValidate({
|
|
62
|
+
values: { host: JMAP_DEFAULT_HOST, email: 'me@example.com', token: '' },
|
|
63
|
+
connector: { id: 'jmap', source: JMAP_DEFAULT_HOST },
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
66
|
+
expect(error).toBeInstanceOf(Error);
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as FetchHttpClient from '@effect/platform/FetchHttpClient';
|
|
6
|
+
import * as Effect from 'effect/Effect';
|
|
7
|
+
import * as Layer from 'effect/Layer';
|
|
8
|
+
import * as Schema from 'effect/Schema';
|
|
9
|
+
|
|
10
|
+
import { Format, Obj, Ref } from '@dxos/echo';
|
|
11
|
+
import { AccessToken } from '@dxos/link';
|
|
12
|
+
import * as Connection from '@dxos/plugin-connector/Connection';
|
|
13
|
+
import * as ConnectorSpec from '@dxos/plugin-connector/ConnectorSpec';
|
|
14
|
+
|
|
15
|
+
import { Jmap } from '../apis';
|
|
16
|
+
import { JMAP_DEFAULT_HOST } from '../constants';
|
|
17
|
+
import { JmapApiError } from '../errors';
|
|
18
|
+
import { JmapCredentials } from '../services';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Manual-credential form for the JMAP connector. JMAP auth is a server-issued Bearer API token
|
|
22
|
+
* (Fastmail: Settings → Privacy & Security → Integrations), outside any OAuth flow, so the host,
|
|
23
|
+
* email, and token are collected directly.
|
|
24
|
+
*/
|
|
25
|
+
const JmapCredentialFormSchema = Schema.Struct({
|
|
26
|
+
host: Schema.String.annotations({
|
|
27
|
+
title: 'Server',
|
|
28
|
+
description: 'JMAP server host. The session is discovered at https://<host>/.well-known/jmap.',
|
|
29
|
+
}),
|
|
30
|
+
email: Schema.String.annotations({
|
|
31
|
+
title: 'Email',
|
|
32
|
+
description: 'Your email address / username on the JMAP server.',
|
|
33
|
+
}),
|
|
34
|
+
token: Schema.String.pipe(Format.FormatAnnotation.set(Format.TypeFormat.Password)).annotations({
|
|
35
|
+
title: 'API token',
|
|
36
|
+
description: 'A JMAP API token, sent as a Bearer credential.',
|
|
37
|
+
}),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
type JmapCredentialFormValues = Schema.Schema.Type<typeof JmapCredentialFormSchema>;
|
|
41
|
+
|
|
42
|
+
export const jmapCredentialForm: ConnectorSpec.CredentialForm<JmapCredentialFormValues> = {
|
|
43
|
+
schema: JmapCredentialFormSchema,
|
|
44
|
+
defaultValues: { host: JMAP_DEFAULT_HOST, email: '', token: '' },
|
|
45
|
+
// Validate against the live session before the dialog closes so a wrong host/token fails inline
|
|
46
|
+
// (with a clear 401 message) instead of creating a Connection that errors on every later sync.
|
|
47
|
+
onValidate: ({ values }) =>
|
|
48
|
+
Effect.gen(function* () {
|
|
49
|
+
const host = values.host.trim();
|
|
50
|
+
const token = values.token.trim();
|
|
51
|
+
if (host.length === 0) {
|
|
52
|
+
return yield* Effect.fail(new Error('Server host is required.'));
|
|
53
|
+
}
|
|
54
|
+
if (token.length === 0) {
|
|
55
|
+
return yield* Effect.fail(new Error('API token is required.'));
|
|
56
|
+
}
|
|
57
|
+
yield* fetchSession(host, token);
|
|
58
|
+
}),
|
|
59
|
+
onSubmit: ({ values, connector }) =>
|
|
60
|
+
Effect.gen(function* () {
|
|
61
|
+
// Trim defensively: onValidate is optional and callers bypass it in tests.
|
|
62
|
+
const host = values.host.trim();
|
|
63
|
+
const email = values.email.trim();
|
|
64
|
+
const token = values.token.trim();
|
|
65
|
+
const session = yield* fetchSession(host, token);
|
|
66
|
+
const { accessToken, connection } = buildJmapCredential({ host, email, token, session, connector });
|
|
67
|
+
return { kind: 'complete' as const, accessToken, connection };
|
|
68
|
+
}),
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Builds the `AccessToken` (source = host, account = form email or session username) and `Connection`
|
|
73
|
+
* for a validated JMAP credential. Pure (no DB), so the construction is unit-testable.
|
|
74
|
+
*/
|
|
75
|
+
export const buildJmapCredential = ({
|
|
76
|
+
host,
|
|
77
|
+
email,
|
|
78
|
+
token,
|
|
79
|
+
session,
|
|
80
|
+
connector,
|
|
81
|
+
}: {
|
|
82
|
+
host: string;
|
|
83
|
+
email: string;
|
|
84
|
+
token: string;
|
|
85
|
+
session: Jmap.Session;
|
|
86
|
+
connector: { id?: string; label?: string };
|
|
87
|
+
}) => {
|
|
88
|
+
const account = email.length > 0 ? email : session.username;
|
|
89
|
+
const accessToken = Obj.make(AccessToken.AccessToken, {
|
|
90
|
+
source: host,
|
|
91
|
+
...(account ? { account } : {}),
|
|
92
|
+
token,
|
|
93
|
+
});
|
|
94
|
+
const connection = Connection.make({
|
|
95
|
+
name: connector.label ?? 'JMAP',
|
|
96
|
+
connectorId: connector.id,
|
|
97
|
+
accessToken: Ref.make(accessToken),
|
|
98
|
+
});
|
|
99
|
+
return { accessToken, connection };
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/** Discovers the JMAP session for the given host/token, surfacing a friendly message on 401. */
|
|
103
|
+
const fetchSession = (host: string, token: string) =>
|
|
104
|
+
Jmap.getSession.pipe(
|
|
105
|
+
Effect.provide(Layer.mergeAll(FetchHttpClient.layer, JmapCredentials.fromValues({ host, token }))),
|
|
106
|
+
Effect.mapError((error) =>
|
|
107
|
+
error instanceof JmapApiError && error.status === 401
|
|
108
|
+
? new Error('The JMAP server rejected the token (401). Check the host and API token and try again.')
|
|
109
|
+
: error instanceof Error
|
|
110
|
+
? error
|
|
111
|
+
: new Error(String(error)),
|
|
112
|
+
),
|
|
113
|
+
);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as ActivationEvents from '@dxos/app-framework/ActivationEvents';
|
|
6
|
+
import * as Capability from '@dxos/app-framework/Capability';
|
|
7
|
+
import * as AppCapability from '@dxos/app-toolkit/AppCapability';
|
|
8
|
+
import * as ConnectorEvents from '@dxos/plugin-connector/ConnectorEvents';
|
|
9
|
+
import * as ConnectorSpec from '@dxos/plugin-connector/ConnectorSpec';
|
|
10
|
+
import * as InboxCapabilities from '@dxos/plugin-inbox/InboxCapabilities';
|
|
11
|
+
|
|
12
|
+
export const Connector = Capability.lazyModule(
|
|
13
|
+
'JmapConnector',
|
|
14
|
+
{ provides: [ConnectorSpec.Connector], activatesOn: ConnectorEvents.Start },
|
|
15
|
+
() => import('./connector'),
|
|
16
|
+
);
|
|
17
|
+
export const MailSend = Capability.lazyModule(
|
|
18
|
+
'JmapMailSend',
|
|
19
|
+
{ provides: [InboxCapabilities.MailSendOperation] },
|
|
20
|
+
() => import('./mail-send'),
|
|
21
|
+
);
|
|
22
|
+
export const OperationHandler = AppCapability.operationHandler(() => import('./operation-handler'), {
|
|
23
|
+
activatesOn: ActivationEvents.Idle,
|
|
24
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Effect from 'effect/Effect';
|
|
6
|
+
|
|
7
|
+
import * as Capability from '@dxos/app-framework/Capability';
|
|
8
|
+
import * as InboxCapabilities from '@dxos/plugin-inbox/InboxCapabilities';
|
|
9
|
+
import * as InboxOperation from '@dxos/plugin-inbox/InboxOperation';
|
|
10
|
+
|
|
11
|
+
import { JMAP_MAIL_CONNECTOR_ID } from '../constants';
|
|
12
|
+
|
|
13
|
+
/** Routes a draft whose mailbox is bound to a JMAP connection to this provider's send operation. */
|
|
14
|
+
export default Capability.makeModule(
|
|
15
|
+
Effect.fnUntraced(function* () {
|
|
16
|
+
return Capability.contribute(InboxCapabilities.MailSendOperation, {
|
|
17
|
+
connectorId: JMAP_MAIL_CONNECTOR_ID,
|
|
18
|
+
getOperation: () => InboxOperation.JmapSend,
|
|
19
|
+
});
|
|
20
|
+
}),
|
|
21
|
+
);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Effect from 'effect/Effect';
|
|
6
|
+
|
|
7
|
+
import * as Capabilities from '@dxos/app-framework/Capabilities';
|
|
8
|
+
import * as Capability from '@dxos/app-framework/Capability';
|
|
9
|
+
|
|
10
|
+
import { JmapOperationHandlerSet } from '#operations';
|
|
11
|
+
|
|
12
|
+
export default Capability.makeModule(
|
|
13
|
+
Effect.fnUntraced(function* () {
|
|
14
|
+
return Capability.contribute(Capabilities.OperationHandler, JmapOperationHandlerSet);
|
|
15
|
+
}),
|
|
16
|
+
);
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
/** `Connector.id` for the JMAP mail connector (RFC 8620/8621); stored as `Connection.connectorId`. */
|
|
6
|
+
export const JMAP_MAIL_CONNECTOR_ID = 'jmap-mail';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Default JMAP server host pre-filled in the credential form. Fastmail is the canonical JMAP
|
|
10
|
+
* provider; the session is discovered at `https://${host}/.well-known/jmap`.
|
|
11
|
+
*/
|
|
12
|
+
export const JMAP_DEFAULT_HOST = 'api.fastmail.com';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The provider's origin domain, used as the foreign-key `Meta.keys[].source` on everything this plugin
|
|
16
|
+
* syncs — messages (the dedup key; see the mapper) and folder {@link Tag}s alike. One string for both
|
|
17
|
+
* is deliberate: a foreign key is scoped by the object it sits on, so the type already distinguishes a
|
|
18
|
+
* message key from a tag key, and `Tag.getOrigin` reads it to mark synced tags read-only.
|
|
19
|
+
*
|
|
20
|
+
* (Gmail is asymmetric here — its message source is the older `com.google.mail` while its tags carry
|
|
21
|
+
* `com.google.gmail`. That is left alone deliberately: renaming a persisted message key would orphan
|
|
22
|
+
* mail. See `Tag.md` §"Origin domain == key source".)
|
|
23
|
+
*/
|
|
24
|
+
export const JMAP_DOMAIN = 'org.ietf.jmap';
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { BaseError, type BaseErrorOptions } from '@dxos/errors';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* JMAP request failed: a non-2xx HTTP status, a JMAP problem-details body, or a method-level
|
|
9
|
+
* `["error", { type, description }, id]` response. `status` carries the HTTP status (when the
|
|
10
|
+
* failure is transport-level) and `type` the JMAP error type (when method-level) so callers can
|
|
11
|
+
* recover with `Effect.catchTag` and distinguish auth (401) from other failures.
|
|
12
|
+
*/
|
|
13
|
+
export class JmapApiError extends BaseError.extend('JmapApiError', 'JMAP API request failed.') {
|
|
14
|
+
constructor(
|
|
15
|
+
public readonly status: number | undefined,
|
|
16
|
+
public readonly detail: string,
|
|
17
|
+
public readonly type?: string,
|
|
18
|
+
options?: BaseErrorOptions,
|
|
19
|
+
) {
|
|
20
|
+
super({ ...options, context: { ...(options?.context ?? {}), status, detail, type } });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** JMAP send payload missing required fields (`to` or body text). */
|
|
25
|
+
export class JmapSendMessageInvalidError extends BaseError.extend(
|
|
26
|
+
'JmapSendMessageInvalidError',
|
|
27
|
+
'Missing "to" or content in message.',
|
|
28
|
+
) {}
|
|
29
|
+
|
|
30
|
+
/** No JMAP identity permits the connection account as a `from` address, so the email cannot be sent. */
|
|
31
|
+
export class JmapSendIdentityNotFoundError extends BaseError.extend(
|
|
32
|
+
'JmapSendIdentityNotFoundError',
|
|
33
|
+
'No JMAP identity matches the connection account.',
|
|
34
|
+
) {
|
|
35
|
+
constructor(account: string | undefined, options?: Omit<BaseErrorOptions, 'context'>) {
|
|
36
|
+
super({ context: { account }, ...options });
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/index.ts
ADDED
package/src/meta.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Operation from '@dxos/compute/Operation';
|
|
6
|
+
import * as OperationHandlerSet from '@dxos/compute/OperationHandlerSet';
|
|
7
|
+
import * as InboxOperation from '@dxos/plugin-inbox/InboxOperation';
|
|
8
|
+
|
|
9
|
+
export const JmapOperationHandlerSet = OperationHandlerSet.lazy([
|
|
10
|
+
InboxOperation.MaterializeJmapTarget.pipe(Operation.lazyHandler(() => import('./mail/materialize/handler'))),
|
|
11
|
+
InboxOperation.JmapSend.pipe(Operation.lazyHandler(() => import('./mail/send'))),
|
|
12
|
+
InboxOperation.JmapSync.pipe(Operation.lazyHandler(() => import('./mail/sync'))),
|
|
13
|
+
]);
|