@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,756 @@
|
|
|
1
|
+
import { t as JmapApiError } from "./chunk-errors.mjs";
|
|
2
|
+
import * as HttpClient from "@effect/platform/HttpClient";
|
|
3
|
+
import * as HttpClientRequest from "@effect/platform/HttpClientRequest";
|
|
4
|
+
import * as HttpClientResponse from "@effect/platform/HttpClientResponse";
|
|
5
|
+
import * as Effect from "effect/Effect";
|
|
6
|
+
import * as Schedule from "effect/Schedule";
|
|
7
|
+
import * as Schema from "effect/Schema";
|
|
8
|
+
import { withAuthorization } from "@dxos/compute-runtime";
|
|
9
|
+
import * as Context from "effect/Context";
|
|
10
|
+
import * as Layer from "effect/Layer";
|
|
11
|
+
import { Database } from "@dxos/echo";
|
|
12
|
+
import { log } from "@dxos/log";
|
|
13
|
+
import { subDays, subMonths, subWeeks, subYears } from "date-fns";
|
|
14
|
+
import * as Match from "effect/Match";
|
|
15
|
+
import * as Option from "effect/Option";
|
|
16
|
+
//#region \0rolldown/runtime.js
|
|
17
|
+
var __defProp = Object.defineProperty;
|
|
18
|
+
var __exportAll = (all, no_symbols) => {
|
|
19
|
+
let target = {};
|
|
20
|
+
for (var name in all) __defProp(target, name, {
|
|
21
|
+
get: all[name],
|
|
22
|
+
enumerable: true
|
|
23
|
+
});
|
|
24
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
25
|
+
return target;
|
|
26
|
+
};
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/services/jmap-credentials.ts
|
|
29
|
+
var __dxlog_file = "/Users/burdon/Code/dxos/dxos/.claude/worktrees/confident-jones-b0b4bf/packages/plugins/plugin-jmap/src/services/jmap-credentials.ts";
|
|
30
|
+
/**
|
|
31
|
+
* Service for accessing JMAP credentials.
|
|
32
|
+
*
|
|
33
|
+
* Mirrors `GoogleCredentials`: an operation invoked with a `Connection` composes
|
|
34
|
+
* `fromConnection(ref)`; one invoked with an external-sync cursor composes
|
|
35
|
+
* `fromAccessToken(cursor.spec.source)` directly (the cursor no longer relates to `Connection`).
|
|
36
|
+
* Unlike Google, there is no database-credential fallback — a JMAP connection always carries host +
|
|
37
|
+
* token in its token record.
|
|
38
|
+
*/
|
|
39
|
+
var JmapCredentials = class JmapCredentials extends Context.Tag("JmapCredentials")() {
|
|
40
|
+
/** Creates a credentials layer from an AccessToken ref. Loads its `source` (host), `account`, `token`. */
|
|
41
|
+
static fromAccessToken = (accessTokenRef) => Layer.effect(JmapCredentials, Effect.gen(function* () {
|
|
42
|
+
const accessToken = yield* Database.load(accessTokenRef);
|
|
43
|
+
log("using access token", { source: accessToken.source }, {
|
|
44
|
+
"~LogMeta": "~LogMeta",
|
|
45
|
+
F: __dxlog_file,
|
|
46
|
+
L: 41,
|
|
47
|
+
S: this
|
|
48
|
+
});
|
|
49
|
+
return {
|
|
50
|
+
host: accessToken.source,
|
|
51
|
+
account: accessToken.account,
|
|
52
|
+
token: accessToken.token
|
|
53
|
+
};
|
|
54
|
+
}));
|
|
55
|
+
/** Creates a credentials layer from a Connection ref. Loads its `accessToken`'s host/account/token. */
|
|
56
|
+
static fromConnection = (connectionRef) => Layer.effect(JmapCredentials, Effect.gen(function* () {
|
|
57
|
+
const connection = yield* Database.load(connectionRef);
|
|
58
|
+
const accessToken = yield* Database.load(connection.accessToken);
|
|
59
|
+
log("using connection access token", { source: accessToken.source }, {
|
|
60
|
+
"~LogMeta": "~LogMeta",
|
|
61
|
+
F: __dxlog_file,
|
|
62
|
+
L: 53,
|
|
63
|
+
S: this
|
|
64
|
+
});
|
|
65
|
+
return {
|
|
66
|
+
host: accessToken.source,
|
|
67
|
+
account: accessToken.account,
|
|
68
|
+
token: accessToken.token
|
|
69
|
+
};
|
|
70
|
+
}));
|
|
71
|
+
/** Creates a credentials layer from explicit values (credential-form validation and tests). */
|
|
72
|
+
static fromValues = (values) => Layer.succeed(JmapCredentials, {
|
|
73
|
+
host: values.host,
|
|
74
|
+
account: values.account,
|
|
75
|
+
token: values.token
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
//#endregion
|
|
79
|
+
//#region src/apis/Jmap/types.ts
|
|
80
|
+
/** Session object returned from `https://${host}/.well-known/jmap` (RFC 8620 §2). */
|
|
81
|
+
var Session = Schema.Struct({
|
|
82
|
+
/** Endpoint for all subsequent API requests. */
|
|
83
|
+
apiUrl: Schema.String,
|
|
84
|
+
/**
|
|
85
|
+
* URI Template (RFC 6570) for blob downloads, with `accountId`/`blobId`/`type`/`name` variables
|
|
86
|
+
* (RFC 8620 §6.2). Required, matching the spec — every real JMAP session advertises one.
|
|
87
|
+
*/
|
|
88
|
+
downloadUrl: Schema.String,
|
|
89
|
+
/** Authenticated username (used to seed the connection account when the form leaves it blank). */
|
|
90
|
+
username: Schema.optional(Schema.String),
|
|
91
|
+
/** Capability urn → primary account id (e.g. `urn:ietf:params:jmap:mail`). */
|
|
92
|
+
primaryAccounts: Schema.Record({
|
|
93
|
+
key: Schema.String,
|
|
94
|
+
value: Schema.String
|
|
95
|
+
}),
|
|
96
|
+
accounts: Schema.optional(Schema.Record({
|
|
97
|
+
key: Schema.String,
|
|
98
|
+
value: Schema.Unknown
|
|
99
|
+
})),
|
|
100
|
+
capabilities: Schema.optional(Schema.Record({
|
|
101
|
+
key: Schema.String,
|
|
102
|
+
value: Schema.Unknown
|
|
103
|
+
})),
|
|
104
|
+
state: Schema.optional(Schema.String)
|
|
105
|
+
});
|
|
106
|
+
/** A method-level error response: `["error", { type, description }, callId]` (RFC 8620 §3.6.1). */
|
|
107
|
+
var MethodError = Schema.Struct({
|
|
108
|
+
type: Schema.String,
|
|
109
|
+
description: Schema.optional(Schema.NullOr(Schema.String))
|
|
110
|
+
});
|
|
111
|
+
/** A single `[name, args, callId]` invocation result; `args` is decoded per-method by the caller. */
|
|
112
|
+
var MethodResponse = Schema.Tuple(Schema.String, Schema.Unknown, Schema.String);
|
|
113
|
+
/** The top-level response envelope of a batched API request (RFC 8620 §3.3). */
|
|
114
|
+
var Response = Schema.Struct({
|
|
115
|
+
methodResponses: Schema.Array(MethodResponse),
|
|
116
|
+
sessionState: Schema.optional(Schema.String)
|
|
117
|
+
});
|
|
118
|
+
//#endregion
|
|
119
|
+
//#region src/apis/Jmap/api.ts
|
|
120
|
+
/** Shared per-attempt timeout/retry policy for JMAP HTTP requests (session, batched API, blob download). */
|
|
121
|
+
var REQUEST_TIMEOUT = "10 seconds";
|
|
122
|
+
var REQUEST_RETRY = Schedule.exponential(1e3).pipe(Schedule.compose(Schedule.recurs(3)));
|
|
123
|
+
var shouldRetry = (error) => !(error instanceof JmapApiError && error.status !== void 0 && error.status >= 400 && error.status < 500);
|
|
124
|
+
/** Capability set for read/write mail requests. */
|
|
125
|
+
var MAIL_CAPABILITIES = ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"];
|
|
126
|
+
/** Capability set including submission, required to send mail. */
|
|
127
|
+
var SUBMISSION_CAPABILITIES = [...MAIL_CAPABILITIES, "urn:ietf:params:jmap:submission"];
|
|
128
|
+
/**
|
|
129
|
+
* Discovers the JMAP session: `GET https://${host}/.well-known/jmap` (the well-known URL
|
|
130
|
+
* 302-redirects to the session resource, which `fetch` follows). Yields `apiUrl` +
|
|
131
|
+
* `primaryAccounts`. Tracer disabled (Effect + CORS: https://github.com/Effect-TS/effect/issues/4568).
|
|
132
|
+
*/
|
|
133
|
+
var getSession = Effect.gen(function* () {
|
|
134
|
+
const { host, token } = yield* JmapCredentials;
|
|
135
|
+
const httpClientWithTracerDisabled = (yield* HttpClient.HttpClient.pipe(Effect.map(withAuthorization(token, "Bearer")))).pipe(HttpClient.withTracerDisabledWhen(() => true));
|
|
136
|
+
return yield* HttpClientRequest.get(`https://${host}/.well-known/jmap`).pipe(HttpClientRequest.setHeader("accept", "application/json"), httpClientWithTracerDisabled.execute, Effect.flatMap(decodeJsonOrFail(Session)), Effect.timeout(REQUEST_TIMEOUT), Effect.retry({
|
|
137
|
+
schedule: REQUEST_RETRY,
|
|
138
|
+
while: shouldRetry
|
|
139
|
+
}), Effect.scoped, Effect.withSpan("JmapGetSession"), Effect.mapError(asJmapApiError));
|
|
140
|
+
});
|
|
141
|
+
/**
|
|
142
|
+
* Issues a batched JMAP API request (single POST to the session `apiUrl`) and decodes the response
|
|
143
|
+
* envelope. Read requests retry on transient failure; pass `{ retry: false }` for non-idempotent
|
|
144
|
+
* writes (draft create + submission) so a retry can't double-send.
|
|
145
|
+
*/
|
|
146
|
+
var jmapRequest = (apiUrl, body, options = {}) => Effect.gen(function* () {
|
|
147
|
+
const { token } = yield* JmapCredentials;
|
|
148
|
+
const httpClientWithTracerDisabled = (yield* HttpClient.HttpClient.pipe(Effect.map(withAuthorization(token, "Bearer")))).pipe(HttpClient.withTracerDisabledWhen(() => true));
|
|
149
|
+
const executed = HttpClientRequest.post(apiUrl).pipe(HttpClientRequest.setHeader("accept", "application/json"), HttpClientRequest.bodyUnsafeJson(body), httpClientWithTracerDisabled.execute, Effect.flatMap(decodeJsonOrFail(Response)), Effect.timeout(REQUEST_TIMEOUT));
|
|
150
|
+
return yield* (options.retry === false ? executed : executed.pipe(Effect.retry({
|
|
151
|
+
schedule: REQUEST_RETRY,
|
|
152
|
+
while: shouldRetry
|
|
153
|
+
}))).pipe(Effect.scoped, Effect.withSpan("JmapApiRequest"), Effect.mapError(asJmapApiError));
|
|
154
|
+
});
|
|
155
|
+
/**
|
|
156
|
+
* Extracts and decodes the result for a given `callId` from a response envelope. A method-level
|
|
157
|
+
* `["error", …]` response (or a decode failure) becomes a typed {@link JmapApiError}.
|
|
158
|
+
*/
|
|
159
|
+
var getMethodResponse = (response, callId, schema) => {
|
|
160
|
+
const entry = response.methodResponses.find((methodResponse) => methodResponse[2] === callId);
|
|
161
|
+
if (!entry) return Effect.fail(new JmapApiError(void 0, `No method response for call ${callId}.`));
|
|
162
|
+
const [name, args] = entry;
|
|
163
|
+
if (name === "error") return Schema.decodeUnknown(MethodError)(args).pipe(Effect.matchEffect({
|
|
164
|
+
onFailure: () => Effect.fail(new JmapApiError(void 0, `JMAP method error (${callId}).`)),
|
|
165
|
+
onSuccess: (error) => Effect.fail(new JmapApiError(void 0, error.description ?? error.type, error.type))
|
|
166
|
+
}));
|
|
167
|
+
return Schema.decodeUnknown(schema)(args).pipe(Effect.mapError(asJmapApiError));
|
|
168
|
+
};
|
|
169
|
+
/** Decodes a 2xx JSON body against `schema`; a >=400 status fails with the (truncated) body text. */
|
|
170
|
+
var decodeJsonOrFail = (schema) => (response) => Effect.gen(function* () {
|
|
171
|
+
if (response.status >= 400) {
|
|
172
|
+
const body = yield* response.text;
|
|
173
|
+
return yield* Effect.fail(new JmapApiError(response.status, truncate(body)));
|
|
174
|
+
}
|
|
175
|
+
return yield* HttpClientResponse.schemaBodyJson(schema)(response);
|
|
176
|
+
}).pipe(Effect.mapError(asJmapApiError));
|
|
177
|
+
/** Collapses transport/decode failures (HttpClientError, ParseError, timeout) into a typed JmapApiError. */
|
|
178
|
+
var asJmapApiError = (error) => error instanceof JmapApiError ? error : new JmapApiError(void 0, error instanceof Error ? error.message : String(error));
|
|
179
|
+
var truncate = (text, max = 500) => text.length > max ? `${text.slice(0, max)}…` : text;
|
|
180
|
+
//#endregion
|
|
181
|
+
//#region src/apis/Jmap/index.ts
|
|
182
|
+
var Jmap_exports = /* @__PURE__ */ __exportAll({
|
|
183
|
+
MAIL_CAPABILITIES: () => MAIL_CAPABILITIES,
|
|
184
|
+
MethodError: () => MethodError,
|
|
185
|
+
MethodResponse: () => MethodResponse,
|
|
186
|
+
REQUEST_RETRY: () => REQUEST_RETRY,
|
|
187
|
+
REQUEST_TIMEOUT: () => REQUEST_TIMEOUT,
|
|
188
|
+
Response: () => Response,
|
|
189
|
+
SUBMISSION_CAPABILITIES: () => SUBMISSION_CAPABILITIES,
|
|
190
|
+
Session: () => Session,
|
|
191
|
+
getMethodResponse: () => getMethodResponse,
|
|
192
|
+
getSession: () => getSession,
|
|
193
|
+
jmapRequest: () => jmapRequest,
|
|
194
|
+
shouldRetry: () => shouldRetry
|
|
195
|
+
});
|
|
196
|
+
//#endregion
|
|
197
|
+
//#region src/apis/JmapMail/types.ts
|
|
198
|
+
/** Structured email address — JMAP gives `{ name, email }` directly (no header parsing). */
|
|
199
|
+
var EmailAddress = Schema.Struct({
|
|
200
|
+
name: Schema.optional(Schema.NullOr(Schema.String)),
|
|
201
|
+
email: Schema.String
|
|
202
|
+
});
|
|
203
|
+
/** A folder. `role` is a well-known role (`inbox`, `drafts`, `sent`, `trash`, …) or null for custom folders. */
|
|
204
|
+
var Mailbox = Schema.Struct({
|
|
205
|
+
id: Schema.String,
|
|
206
|
+
name: Schema.String,
|
|
207
|
+
role: Schema.optional(Schema.NullOr(Schema.String)),
|
|
208
|
+
parentId: Schema.optional(Schema.NullOr(Schema.String)),
|
|
209
|
+
sortOrder: Schema.optional(Schema.Number),
|
|
210
|
+
totalEmails: Schema.optional(Schema.Number),
|
|
211
|
+
unreadEmails: Schema.optional(Schema.Number)
|
|
212
|
+
});
|
|
213
|
+
/** A decoded body part value (when fetched with `fetchTextBodyValues`). */
|
|
214
|
+
var BodyValue = Schema.Struct({
|
|
215
|
+
value: Schema.String,
|
|
216
|
+
isEncodingProblem: Schema.optional(Schema.Boolean),
|
|
217
|
+
isTruncated: Schema.optional(Schema.Boolean)
|
|
218
|
+
});
|
|
219
|
+
/** A body part reference (the `partId` keys into `Email.bodyValues`; `blobId` fetches raw bytes). */
|
|
220
|
+
var EmailBodyPart = Schema.Struct({
|
|
221
|
+
partId: Schema.optional(Schema.NullOr(Schema.String)),
|
|
222
|
+
blobId: Schema.optional(Schema.NullOr(Schema.String)),
|
|
223
|
+
size: Schema.optional(Schema.Number),
|
|
224
|
+
type: Schema.optional(Schema.String),
|
|
225
|
+
charset: Schema.optional(Schema.NullOr(Schema.String)),
|
|
226
|
+
/** Attachment filename, present on parts listed in `Email.attachments`. */
|
|
227
|
+
name: Schema.optional(Schema.NullOr(Schema.String)),
|
|
228
|
+
/** MIME `Content-Disposition` (`attachment` or `inline`), present on parts listed in `Email.attachments`. */
|
|
229
|
+
disposition: Schema.optional(Schema.NullOr(Schema.String)),
|
|
230
|
+
/** The part's `Content-ID` header (RFC 8621 §4.1.4), if any — matches a `cid:` reference in an HTML body. */
|
|
231
|
+
cid: Schema.optional(Schema.NullOr(Schema.String))
|
|
232
|
+
});
|
|
233
|
+
/** An email (RFC 8621 §4). Only the properties requested in `Email/get` are present. */
|
|
234
|
+
var Email = Schema.Struct({
|
|
235
|
+
id: Schema.String,
|
|
236
|
+
blobId: Schema.optional(Schema.String),
|
|
237
|
+
threadId: Schema.String,
|
|
238
|
+
mailboxIds: Schema.optional(Schema.Record({
|
|
239
|
+
key: Schema.String,
|
|
240
|
+
value: Schema.Boolean
|
|
241
|
+
})),
|
|
242
|
+
keywords: Schema.optional(Schema.Record({
|
|
243
|
+
key: Schema.String,
|
|
244
|
+
value: Schema.Boolean
|
|
245
|
+
})),
|
|
246
|
+
from: Schema.optional(Schema.NullOr(Schema.Array(EmailAddress))),
|
|
247
|
+
to: Schema.optional(Schema.NullOr(Schema.Array(EmailAddress))),
|
|
248
|
+
cc: Schema.optional(Schema.NullOr(Schema.Array(EmailAddress))),
|
|
249
|
+
bcc: Schema.optional(Schema.NullOr(Schema.Array(EmailAddress))),
|
|
250
|
+
subject: Schema.optional(Schema.NullOr(Schema.String)),
|
|
251
|
+
receivedAt: Schema.String,
|
|
252
|
+
sentAt: Schema.optional(Schema.NullOr(Schema.String)),
|
|
253
|
+
preview: Schema.optional(Schema.NullOr(Schema.String)),
|
|
254
|
+
messageId: Schema.optional(Schema.NullOr(Schema.Array(Schema.String))),
|
|
255
|
+
inReplyTo: Schema.optional(Schema.NullOr(Schema.Array(Schema.String))),
|
|
256
|
+
references: Schema.optional(Schema.NullOr(Schema.Array(Schema.String))),
|
|
257
|
+
bodyValues: Schema.optional(Schema.Record({
|
|
258
|
+
key: Schema.String,
|
|
259
|
+
value: BodyValue
|
|
260
|
+
})),
|
|
261
|
+
textBody: Schema.optional(Schema.Array(EmailBodyPart)),
|
|
262
|
+
htmlBody: Schema.optional(Schema.Array(EmailBodyPart)),
|
|
263
|
+
/** Non-body parts (RFC 8621 §4.1.4) — the attachments proper, distinct from `textBody`/`htmlBody`. */
|
|
264
|
+
attachments: Schema.optional(Schema.Array(EmailBodyPart))
|
|
265
|
+
});
|
|
266
|
+
/** A sending identity (RFC 8621 §6); `email` is an address the server permits as `from`. */
|
|
267
|
+
var Identity = Schema.Struct({
|
|
268
|
+
id: Schema.String,
|
|
269
|
+
name: Schema.optional(Schema.NullOr(Schema.String)),
|
|
270
|
+
email: Schema.String
|
|
271
|
+
});
|
|
272
|
+
var MailboxGetResult = Schema.Struct({
|
|
273
|
+
accountId: Schema.optional(Schema.String),
|
|
274
|
+
state: Schema.optional(Schema.String),
|
|
275
|
+
list: Schema.Array(Mailbox),
|
|
276
|
+
notFound: Schema.optional(Schema.NullOr(Schema.Array(Schema.String)))
|
|
277
|
+
});
|
|
278
|
+
var EmailQueryResult = Schema.Struct({
|
|
279
|
+
accountId: Schema.optional(Schema.String),
|
|
280
|
+
queryState: Schema.optional(Schema.String),
|
|
281
|
+
position: Schema.optional(Schema.Number),
|
|
282
|
+
total: Schema.optional(Schema.Number),
|
|
283
|
+
limit: Schema.optional(Schema.Number),
|
|
284
|
+
ids: Schema.Array(Schema.String)
|
|
285
|
+
});
|
|
286
|
+
var EmailGetResult = Schema.Struct({
|
|
287
|
+
accountId: Schema.optional(Schema.String),
|
|
288
|
+
state: Schema.optional(Schema.String),
|
|
289
|
+
list: Schema.Array(Email),
|
|
290
|
+
notFound: Schema.optional(Schema.NullOr(Schema.Array(Schema.String)))
|
|
291
|
+
});
|
|
292
|
+
/** `Email/changes` delta since an opaque `sinceState` token (RFC 8621 §4.3 / RFC 8620 §5.2). */
|
|
293
|
+
var EmailChangesResult = Schema.Struct({
|
|
294
|
+
accountId: Schema.optional(Schema.String),
|
|
295
|
+
oldState: Schema.String,
|
|
296
|
+
newState: Schema.String,
|
|
297
|
+
hasMoreChanges: Schema.Boolean,
|
|
298
|
+
created: Schema.Array(Schema.String),
|
|
299
|
+
updated: Schema.Array(Schema.String),
|
|
300
|
+
destroyed: Schema.Array(Schema.String)
|
|
301
|
+
});
|
|
302
|
+
var IdentityGetResult = Schema.Struct({
|
|
303
|
+
accountId: Schema.optional(Schema.String),
|
|
304
|
+
state: Schema.optional(Schema.String),
|
|
305
|
+
list: Schema.Array(Identity),
|
|
306
|
+
notFound: Schema.optional(Schema.NullOr(Schema.Array(Schema.String)))
|
|
307
|
+
});
|
|
308
|
+
var EmailSetResult = Schema.Struct({
|
|
309
|
+
accountId: Schema.optional(Schema.String),
|
|
310
|
+
oldState: Schema.optional(Schema.NullOr(Schema.String)),
|
|
311
|
+
newState: Schema.optional(Schema.String),
|
|
312
|
+
created: Schema.optional(Schema.NullOr(Schema.Record({
|
|
313
|
+
key: Schema.String,
|
|
314
|
+
value: Schema.Struct({
|
|
315
|
+
id: Schema.String,
|
|
316
|
+
blobId: Schema.optional(Schema.String),
|
|
317
|
+
threadId: Schema.optional(Schema.String),
|
|
318
|
+
size: Schema.optional(Schema.Number)
|
|
319
|
+
})
|
|
320
|
+
}))),
|
|
321
|
+
updated: Schema.optional(Schema.NullOr(Schema.Record({
|
|
322
|
+
key: Schema.String,
|
|
323
|
+
value: Schema.Unknown
|
|
324
|
+
}))),
|
|
325
|
+
destroyed: Schema.optional(Schema.NullOr(Schema.Array(Schema.String))),
|
|
326
|
+
notCreated: Schema.optional(Schema.NullOr(Schema.Record({
|
|
327
|
+
key: Schema.String,
|
|
328
|
+
value: Schema.Struct({
|
|
329
|
+
type: Schema.String,
|
|
330
|
+
description: Schema.optional(Schema.NullOr(Schema.String))
|
|
331
|
+
})
|
|
332
|
+
}))),
|
|
333
|
+
notUpdated: Schema.optional(Schema.NullOr(Schema.Record({
|
|
334
|
+
key: Schema.String,
|
|
335
|
+
value: Schema.Struct({
|
|
336
|
+
type: Schema.String,
|
|
337
|
+
description: Schema.optional(Schema.NullOr(Schema.String))
|
|
338
|
+
})
|
|
339
|
+
}))),
|
|
340
|
+
notDestroyed: Schema.optional(Schema.NullOr(Schema.Record({
|
|
341
|
+
key: Schema.String,
|
|
342
|
+
value: Schema.Struct({
|
|
343
|
+
type: Schema.String,
|
|
344
|
+
description: Schema.optional(Schema.NullOr(Schema.String))
|
|
345
|
+
})
|
|
346
|
+
})))
|
|
347
|
+
});
|
|
348
|
+
var EmailSubmissionSetResult = Schema.Struct({
|
|
349
|
+
accountId: Schema.optional(Schema.String),
|
|
350
|
+
newState: Schema.optional(Schema.String),
|
|
351
|
+
created: Schema.optional(Schema.NullOr(Schema.Record({
|
|
352
|
+
key: Schema.String,
|
|
353
|
+
value: Schema.Struct({ id: Schema.String })
|
|
354
|
+
}))),
|
|
355
|
+
notCreated: Schema.optional(Schema.NullOr(Schema.Record({
|
|
356
|
+
key: Schema.String,
|
|
357
|
+
value: Schema.Struct({
|
|
358
|
+
type: Schema.String,
|
|
359
|
+
description: Schema.optional(Schema.NullOr(Schema.String))
|
|
360
|
+
})
|
|
361
|
+
})))
|
|
362
|
+
});
|
|
363
|
+
//#endregion
|
|
364
|
+
//#region src/apis/JmapMail/api.ts
|
|
365
|
+
/** Expands a JMAP `downloadUrl` URI Template (RFC 6570 level 1) with the given variables. */
|
|
366
|
+
var expandDownloadUrl = (template, variables) => template.replace(/\{(\w+)\}/g, (_, key) => key in variables ? encodeURIComponent(variables[key]) : "");
|
|
367
|
+
/**
|
|
368
|
+
* Downloads a blob's raw bytes via the session's `downloadUrl` template. `name`/`type` are advisory
|
|
369
|
+
* (they only affect the response's `Content-Disposition`/`Accept`), not required for correctness.
|
|
370
|
+
*/
|
|
371
|
+
var downloadBlob = Effect.fn("downloadBlob")(function* (target, blobId, options = {}) {
|
|
372
|
+
if (!target.downloadUrl) return yield* Effect.fail(new JmapApiError(void 0, "Session has no downloadUrl."));
|
|
373
|
+
const url = expandDownloadUrl(target.downloadUrl, {
|
|
374
|
+
accountId: target.accountId,
|
|
375
|
+
blobId,
|
|
376
|
+
name: options.name ?? blobId,
|
|
377
|
+
type: options.type ?? "application/octet-stream"
|
|
378
|
+
});
|
|
379
|
+
const { token } = yield* JmapCredentials;
|
|
380
|
+
const httpClient = yield* HttpClient.HttpClient.pipe(Effect.map(withAuthorization(token, "Bearer")));
|
|
381
|
+
const buffer = yield* HttpClientRequest.get(url).pipe(httpClient.execute, Effect.flatMap((response) => response.arrayBuffer), Effect.mapError(asJmapDownloadError), Effect.timeout(REQUEST_TIMEOUT), Effect.retry({
|
|
382
|
+
schedule: REQUEST_RETRY,
|
|
383
|
+
while: shouldRetry
|
|
384
|
+
}), Effect.scoped, Effect.mapError(asJmapDownloadError));
|
|
385
|
+
return new Uint8Array(buffer);
|
|
386
|
+
});
|
|
387
|
+
/** Collapses transport/decode failures into a typed {@link JmapApiError} (mirrors `Jmap/api.ts`). */
|
|
388
|
+
var asJmapDownloadError = (error) => error instanceof JmapApiError ? error : new JmapApiError(void 0, error instanceof Error ? error.message : String(error));
|
|
389
|
+
/** Email properties fetched by {@link emailGet} — enough for the mapper to build a Message. */
|
|
390
|
+
var EMAIL_PROPERTIES = [
|
|
391
|
+
"id",
|
|
392
|
+
"blobId",
|
|
393
|
+
"threadId",
|
|
394
|
+
"mailboxIds",
|
|
395
|
+
"keywords",
|
|
396
|
+
"from",
|
|
397
|
+
"to",
|
|
398
|
+
"cc",
|
|
399
|
+
"subject",
|
|
400
|
+
"receivedAt",
|
|
401
|
+
"sentAt",
|
|
402
|
+
"preview",
|
|
403
|
+
"messageId",
|
|
404
|
+
"inReplyTo",
|
|
405
|
+
"references",
|
|
406
|
+
"bodyValues",
|
|
407
|
+
"textBody",
|
|
408
|
+
"htmlBody",
|
|
409
|
+
"attachments"
|
|
410
|
+
];
|
|
411
|
+
/** Lists all folders (mailboxes) in the account (RFC 8621 §2.3). */
|
|
412
|
+
var mailboxGet = Effect.fn("mailboxGet")(function* (target) {
|
|
413
|
+
return yield* getMethodResponse(yield* jmapRequest(target.apiUrl, {
|
|
414
|
+
using: MAIL_CAPABILITIES,
|
|
415
|
+
methodCalls: [[
|
|
416
|
+
"Mailbox/get",
|
|
417
|
+
{
|
|
418
|
+
accountId: target.accountId,
|
|
419
|
+
ids: null
|
|
420
|
+
},
|
|
421
|
+
"0"
|
|
422
|
+
]]
|
|
423
|
+
}), "0", MailboxGetResult);
|
|
424
|
+
});
|
|
425
|
+
/** Queries email ids matching a filter (RFC 8621 §4.4). */
|
|
426
|
+
var emailQuery = Effect.fn("emailQuery")(function* (target, options = {}) {
|
|
427
|
+
const args = { accountId: target.accountId };
|
|
428
|
+
if (options.filter !== void 0) args.filter = options.filter;
|
|
429
|
+
if (options.sort !== void 0) args.sort = options.sort;
|
|
430
|
+
if (options.position !== void 0) args.position = options.position;
|
|
431
|
+
if (options.limit !== void 0) args.limit = options.limit;
|
|
432
|
+
if (options.calculateTotal !== void 0) args.calculateTotal = options.calculateTotal;
|
|
433
|
+
return yield* getMethodResponse(yield* jmapRequest(target.apiUrl, {
|
|
434
|
+
using: MAIL_CAPABILITIES,
|
|
435
|
+
methodCalls: [[
|
|
436
|
+
"Email/query",
|
|
437
|
+
args,
|
|
438
|
+
"0"
|
|
439
|
+
]]
|
|
440
|
+
}), "0", EmailQueryResult);
|
|
441
|
+
});
|
|
442
|
+
/** Fetches emails by id with decoded HTML and text body values (RFC 8621 §4.2). */
|
|
443
|
+
var emailGet = Effect.fn("emailGet")(function* (target, ids, properties = EMAIL_PROPERTIES) {
|
|
444
|
+
return yield* getMethodResponse(yield* jmapRequest(target.apiUrl, {
|
|
445
|
+
using: MAIL_CAPABILITIES,
|
|
446
|
+
methodCalls: [[
|
|
447
|
+
"Email/get",
|
|
448
|
+
{
|
|
449
|
+
accountId: target.accountId,
|
|
450
|
+
ids,
|
|
451
|
+
properties,
|
|
452
|
+
fetchTextBodyValues: true,
|
|
453
|
+
fetchHTMLBodyValues: true
|
|
454
|
+
},
|
|
455
|
+
"0"
|
|
456
|
+
]]
|
|
457
|
+
}), "0", EmailGetResult);
|
|
458
|
+
});
|
|
459
|
+
/**
|
|
460
|
+
* Fetches the delta of email changes since an opaque `sinceState` token (RFC 8621 §4.3). A server that
|
|
461
|
+
* can't compute the delta (state too old / evicted) returns a `cannotCalculateChanges` method error,
|
|
462
|
+
* surfaced as a {@link JmapApiError} with that `type` — the caller's cue to fall back to a full scan.
|
|
463
|
+
* The token to pass comes from a prior `Email/get` `state` (NOT `Email/query` `queryState`).
|
|
464
|
+
*/
|
|
465
|
+
var emailChanges = Effect.fn("emailChanges")(function* (target, sinceState, maxChanges) {
|
|
466
|
+
const args = {
|
|
467
|
+
accountId: target.accountId,
|
|
468
|
+
sinceState
|
|
469
|
+
};
|
|
470
|
+
if (maxChanges !== void 0) args.maxChanges = maxChanges;
|
|
471
|
+
return yield* getMethodResponse(yield* jmapRequest(target.apiUrl, {
|
|
472
|
+
using: MAIL_CAPABILITIES,
|
|
473
|
+
methodCalls: [[
|
|
474
|
+
"Email/changes",
|
|
475
|
+
args,
|
|
476
|
+
"0"
|
|
477
|
+
]]
|
|
478
|
+
}), "0", EmailChangesResult);
|
|
479
|
+
});
|
|
480
|
+
/** Lists the account's sending identities (RFC 8621 §6.2). */
|
|
481
|
+
var identityGet = Effect.fn("identityGet")(function* (target) {
|
|
482
|
+
return yield* getMethodResponse(yield* jmapRequest(target.apiUrl, {
|
|
483
|
+
using: SUBMISSION_CAPABILITIES,
|
|
484
|
+
methodCalls: [[
|
|
485
|
+
"Identity/get",
|
|
486
|
+
{
|
|
487
|
+
accountId: target.accountId,
|
|
488
|
+
ids: null
|
|
489
|
+
},
|
|
490
|
+
"0"
|
|
491
|
+
]]
|
|
492
|
+
}), "0", IdentityGetResult);
|
|
493
|
+
});
|
|
494
|
+
/** Patches a single email (e.g. replace `mailboxIds` to move to Trash). Idempotent, so retryable. */
|
|
495
|
+
var emailSetUpdate = Effect.fn("emailSetUpdate")(function* (target, emailId, patch) {
|
|
496
|
+
const result = yield* getMethodResponse(yield* jmapRequest(target.apiUrl, {
|
|
497
|
+
using: MAIL_CAPABILITIES,
|
|
498
|
+
methodCalls: [[
|
|
499
|
+
"Email/set",
|
|
500
|
+
{
|
|
501
|
+
accountId: target.accountId,
|
|
502
|
+
update: { [emailId]: patch }
|
|
503
|
+
},
|
|
504
|
+
"0"
|
|
505
|
+
]]
|
|
506
|
+
}), "0", EmailSetResult);
|
|
507
|
+
const error = result.notUpdated?.[emailId];
|
|
508
|
+
if (error) return yield* Effect.fail(new JmapApiError(void 0, error.description ?? error.type, error.type));
|
|
509
|
+
return result;
|
|
510
|
+
});
|
|
511
|
+
/**
|
|
512
|
+
* Creates a draft and submits it for delivery in a single batched request. Not retried (a retry
|
|
513
|
+
* could double-send). Returns the created email's id and thread id.
|
|
514
|
+
*/
|
|
515
|
+
var submitEmail = Effect.fn("submitEmail")(function* (target, args) {
|
|
516
|
+
const { identityId, draftsMailboxId, sentMailboxId, draft } = args;
|
|
517
|
+
const draftCreate = {
|
|
518
|
+
mailboxIds: { [draftsMailboxId]: true },
|
|
519
|
+
keywords: {
|
|
520
|
+
$draft: true,
|
|
521
|
+
$seen: true
|
|
522
|
+
},
|
|
523
|
+
from: draft.from,
|
|
524
|
+
to: draft.to,
|
|
525
|
+
bodyValues: { body: { value: draft.text } },
|
|
526
|
+
textBody: [{
|
|
527
|
+
partId: "body",
|
|
528
|
+
type: "text/plain"
|
|
529
|
+
}]
|
|
530
|
+
};
|
|
531
|
+
if (draft.cc && draft.cc.length > 0) draftCreate.cc = draft.cc;
|
|
532
|
+
if (draft.bcc && draft.bcc.length > 0) draftCreate.bcc = draft.bcc;
|
|
533
|
+
if (draft.subject !== void 0) draftCreate.subject = draft.subject;
|
|
534
|
+
if (draft.inReplyTo && draft.inReplyTo.length > 0) draftCreate.inReplyTo = draft.inReplyTo;
|
|
535
|
+
if (draft.references && draft.references.length > 0) draftCreate.references = draft.references;
|
|
536
|
+
const response = yield* jmapRequest(target.apiUrl, {
|
|
537
|
+
using: SUBMISSION_CAPABILITIES,
|
|
538
|
+
methodCalls: [[
|
|
539
|
+
"Email/set",
|
|
540
|
+
{
|
|
541
|
+
accountId: target.accountId,
|
|
542
|
+
create: { draft: draftCreate }
|
|
543
|
+
},
|
|
544
|
+
"0"
|
|
545
|
+
], [
|
|
546
|
+
"EmailSubmission/set",
|
|
547
|
+
{
|
|
548
|
+
accountId: target.accountId,
|
|
549
|
+
create: { sub: {
|
|
550
|
+
emailId: "#draft",
|
|
551
|
+
identityId
|
|
552
|
+
} },
|
|
553
|
+
onSuccessUpdateEmail: { "#sub": {
|
|
554
|
+
[`mailboxIds/${draftsMailboxId}`]: null,
|
|
555
|
+
[`mailboxIds/${sentMailboxId}`]: true,
|
|
556
|
+
"keywords/$draft": null
|
|
557
|
+
} }
|
|
558
|
+
},
|
|
559
|
+
"1"
|
|
560
|
+
]]
|
|
561
|
+
}, { retry: false });
|
|
562
|
+
const setResult = yield* getMethodResponse(response, "0", EmailSetResult);
|
|
563
|
+
const created = setResult.created?.draft;
|
|
564
|
+
if (!created) {
|
|
565
|
+
const error = setResult.notCreated?.draft;
|
|
566
|
+
return yield* Effect.fail(new JmapApiError(void 0, error?.description ?? error?.type ?? "Draft was not created.", error?.type));
|
|
567
|
+
}
|
|
568
|
+
const submissionResult = yield* getMethodResponse(response, "1", EmailSubmissionSetResult);
|
|
569
|
+
if (!submissionResult.created?.sub) {
|
|
570
|
+
const error = submissionResult.notCreated?.sub;
|
|
571
|
+
return yield* Effect.fail(new JmapApiError(void 0, error?.description ?? error?.type ?? "Email submission failed.", error?.type));
|
|
572
|
+
}
|
|
573
|
+
return {
|
|
574
|
+
id: created.id,
|
|
575
|
+
threadId: created.threadId
|
|
576
|
+
};
|
|
577
|
+
});
|
|
578
|
+
//#endregion
|
|
579
|
+
//#region src/apis/JmapMail/query.ts
|
|
580
|
+
/**
|
|
581
|
+
* Translates a Gmail-like query string into a JMAP `Email/query` filter (RFC 8621 §4.4.1).
|
|
582
|
+
*
|
|
583
|
+
* Supported operators: `from:`/`to:`/`cc:`/`bcc:`/`subject:`/`body:`, `label:`/`in:` (resolved to a
|
|
584
|
+
* mailbox id), `is:unread|read|starred|flagged|unstarred|draft|answered`, `has:attachment`,
|
|
585
|
+
* `after:`/`before:` (`YYYY/MM/DD` or `YYYY-MM-DD`), `newer_than:`/`older_than:` (e.g. `7d`, `2w`,
|
|
586
|
+
* `3m`, `1y`), `larger:`/`smaller:` (e.g. `10M`, `500k`), `list:`/`deliveredto:` (header match),
|
|
587
|
+
* quoted phrases, `-` negation, `OR` groups, and bare words (full-text). Space-separated terms are
|
|
588
|
+
* AND-ed. Returns `None` for an empty query, or one whose terms all fail to resolve (e.g. an unknown
|
|
589
|
+
* label).
|
|
590
|
+
*/
|
|
591
|
+
var parseMailQuery = (query, ctx) => buildFilter(tokenize(query), ctx);
|
|
592
|
+
/** Resolves a label/folder name (case-insensitive) or a system role/alias to a mailbox id. */
|
|
593
|
+
var resolveMailboxByNameOrRole = (folders, nameOrRole) => {
|
|
594
|
+
const lowered = nameOrRole.toLowerCase();
|
|
595
|
+
const role = ROLE_ALIASES[lowered] ?? lowered;
|
|
596
|
+
const match = folders.find((folder) => folder.role === role) ?? folders.find((folder) => folder.name.toLowerCase() === lowered);
|
|
597
|
+
return Option.fromNullable(match?.id);
|
|
598
|
+
};
|
|
599
|
+
var TOKEN_RE = /-?(?:[A-Za-z_]+:)?"[^"]*"|\S+/g;
|
|
600
|
+
var tokenize = (query) => (query.match(TOKEN_RE) ?? []).map(parseToken);
|
|
601
|
+
var parseToken = (raw) => {
|
|
602
|
+
if (raw === "OR") return { _tag: "or" };
|
|
603
|
+
const negate = raw.startsWith("-");
|
|
604
|
+
const rest = negate ? raw.slice(1) : raw;
|
|
605
|
+
const colon = rest.indexOf(":");
|
|
606
|
+
if (colon > 0 && /^[A-Za-z_]+$/.test(rest.slice(0, colon))) return {
|
|
607
|
+
_tag: "term",
|
|
608
|
+
negate,
|
|
609
|
+
op: Option.some(rest.slice(0, colon).toLowerCase()),
|
|
610
|
+
value: stripQuotes(rest.slice(colon + 1))
|
|
611
|
+
};
|
|
612
|
+
return {
|
|
613
|
+
_tag: "term",
|
|
614
|
+
negate,
|
|
615
|
+
op: Option.none(),
|
|
616
|
+
value: stripQuotes(rest)
|
|
617
|
+
};
|
|
618
|
+
};
|
|
619
|
+
var stripQuotes = (value) => value.length >= 2 && value.startsWith("\"") && value.endsWith("\"") ? value.slice(1, -1) : value;
|
|
620
|
+
var buildFilter = (tokens, ctx) => {
|
|
621
|
+
const { operands } = tokens.reduce((state, token) => {
|
|
622
|
+
if (token._tag === "or") return {
|
|
623
|
+
...state,
|
|
624
|
+
pendingOr: state.operands.length > 0
|
|
625
|
+
};
|
|
626
|
+
const leaf = buildTerm(token, ctx);
|
|
627
|
+
if (Option.isNone(leaf)) return {
|
|
628
|
+
...state,
|
|
629
|
+
pendingOr: false
|
|
630
|
+
};
|
|
631
|
+
const previous = state.pendingOr ? state.operands[state.operands.length - 1] : void 0;
|
|
632
|
+
return previous !== void 0 ? {
|
|
633
|
+
operands: [...state.operands.slice(0, -1), mergeOr(previous, leaf.value)],
|
|
634
|
+
pendingOr: false
|
|
635
|
+
} : {
|
|
636
|
+
operands: [...state.operands, leaf.value],
|
|
637
|
+
pendingOr: false
|
|
638
|
+
};
|
|
639
|
+
}, {
|
|
640
|
+
operands: [],
|
|
641
|
+
pendingOr: false
|
|
642
|
+
});
|
|
643
|
+
return Match.value(operands.length).pipe(Match.when(0, () => Option.none()), Match.when(1, () => Option.fromNullable(operands[0])), Match.orElse(() => Option.some({
|
|
644
|
+
operator: "AND",
|
|
645
|
+
conditions: operands
|
|
646
|
+
})));
|
|
647
|
+
};
|
|
648
|
+
var mergeOr = (previous, next) => isOperator(previous) && previous.operator === "OR" ? {
|
|
649
|
+
operator: "OR",
|
|
650
|
+
conditions: [...previous.conditions, next]
|
|
651
|
+
} : {
|
|
652
|
+
operator: "OR",
|
|
653
|
+
conditions: [previous, next]
|
|
654
|
+
};
|
|
655
|
+
var isOperator = (filter) => "operator" in filter;
|
|
656
|
+
/** Whether a filter restricts the mailbox scope (`inMailbox`/`inMailboxOtherThan`), recursively. */
|
|
657
|
+
var filterScopesMailbox = (filter) => isOperator(filter) ? filter.conditions.some(filterScopesMailbox) : filter.inMailbox !== void 0 || filter.inMailboxOtherThan !== void 0;
|
|
658
|
+
var buildTerm = (token, ctx) => buildLeaf(token.op, token.value, ctx).pipe(Option.map((leaf) => token.negate ? negate(leaf) : leaf));
|
|
659
|
+
var negate = (filter) => ({
|
|
660
|
+
operator: "NOT",
|
|
661
|
+
conditions: [filter]
|
|
662
|
+
});
|
|
663
|
+
var buildLeaf = (op, value, ctx) => op.pipe(Option.match({
|
|
664
|
+
onNone: () => textLeaf(value),
|
|
665
|
+
onSome: (operator) => buildOperatorLeaf(operator, value, ctx)
|
|
666
|
+
}));
|
|
667
|
+
var buildOperatorLeaf = (operator, value, ctx) => Match.value(operator).pipe(Match.whenOr("from", "to", "cc", "bcc", "subject", "body", (field) => condLeaf(value, STRING_FIELD_LEAVES[field](value))), Match.whenOr("label", "in", () => ctx.resolveMailbox(value).pipe(Option.map((id) => ({ inMailbox: id })))), Match.when("is", () => Option.fromNullable(IS_LEAVES[value.toLowerCase()])), Match.when("has", () => value.toLowerCase() === "attachment" ? Option.some({ hasAttachment: true }) : Option.none()), Match.whenOr("after", "newer", () => dateLeaf(value, "after")), Match.whenOr("before", "older", () => dateLeaf(value, "before")), Match.when("newer_than", () => relativeDateLeaf(value, ctx.now, "after")), Match.when("older_than", () => relativeDateLeaf(value, ctx.now, "before")), Match.when("larger", () => sizeLeaf(value, "minSize")), Match.when("smaller", () => sizeLeaf(value, "maxSize")), Match.when("list", () => headerLeaf("List-Id", value)), Match.when("deliveredto", () => headerLeaf("Delivered-To", value)), Match.orElse(() => textLeaf(`${operator}:${value}`)));
|
|
668
|
+
var STRING_FIELD_LEAVES = {
|
|
669
|
+
from: (value) => ({ from: value }),
|
|
670
|
+
to: (value) => ({ to: value }),
|
|
671
|
+
cc: (value) => ({ cc: value }),
|
|
672
|
+
bcc: (value) => ({ bcc: value }),
|
|
673
|
+
subject: (value) => ({ subject: value }),
|
|
674
|
+
body: (value) => ({ body: value })
|
|
675
|
+
};
|
|
676
|
+
var IS_LEAVES = {
|
|
677
|
+
unread: { notKeyword: "$seen" },
|
|
678
|
+
read: { hasKeyword: "$seen" },
|
|
679
|
+
starred: { hasKeyword: "$flagged" },
|
|
680
|
+
flagged: { hasKeyword: "$flagged" },
|
|
681
|
+
unstarred: { notKeyword: "$flagged" },
|
|
682
|
+
unflagged: { notKeyword: "$flagged" },
|
|
683
|
+
draft: { hasKeyword: "$draft" },
|
|
684
|
+
answered: { hasKeyword: "$answered" },
|
|
685
|
+
replied: { hasKeyword: "$answered" }
|
|
686
|
+
};
|
|
687
|
+
var ROLE_ALIASES = {
|
|
688
|
+
spam: "junk",
|
|
689
|
+
bin: "trash",
|
|
690
|
+
deleted: "trash"
|
|
691
|
+
};
|
|
692
|
+
var SIZE_UNITS = {
|
|
693
|
+
"": 1,
|
|
694
|
+
"k": 1024,
|
|
695
|
+
"m": 1024 * 1024,
|
|
696
|
+
"g": 1024 * 1024 * 1024
|
|
697
|
+
};
|
|
698
|
+
var DATE_RE = /^(\d{4})[/-](\d{1,2})[/-](\d{1,2})$/;
|
|
699
|
+
var RELATIVE_RE = /^(\d+)([dwmy])$/;
|
|
700
|
+
var SIZE_RE = /^(\d+)([kmg]?)b?$/i;
|
|
701
|
+
/** Wraps a built condition in `Some`, or `None` when the value is blank. */
|
|
702
|
+
var condLeaf = (value, condition) => value.trim().length === 0 ? Option.none() : Option.some(condition);
|
|
703
|
+
var textLeaf = (value) => condLeaf(value, { text: value });
|
|
704
|
+
var headerLeaf = (name, value) => condLeaf(value, { header: [name, value] });
|
|
705
|
+
var dateLeaf = (value, bound) => {
|
|
706
|
+
const match = DATE_RE.exec(value);
|
|
707
|
+
if (!match) return Option.none();
|
|
708
|
+
const iso = new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]))).toISOString();
|
|
709
|
+
return Option.some(bound === "after" ? { after: iso } : { before: iso });
|
|
710
|
+
};
|
|
711
|
+
var relativeDateLeaf = (value, now, bound) => {
|
|
712
|
+
const match = RELATIVE_RE.exec(value.toLowerCase());
|
|
713
|
+
if (!match) return Option.none();
|
|
714
|
+
const amount = Number(match[1]);
|
|
715
|
+
const iso = Match.value(match[2]).pipe(Match.when("w", () => subWeeks(now, amount)), Match.when("m", () => subMonths(now, amount)), Match.when("y", () => subYears(now, amount)), Match.orElse(() => subDays(now, amount))).toISOString();
|
|
716
|
+
return Option.some(bound === "after" ? { after: iso } : { before: iso });
|
|
717
|
+
};
|
|
718
|
+
var sizeLeaf = (value, bound) => {
|
|
719
|
+
const match = SIZE_RE.exec(value.toLowerCase());
|
|
720
|
+
if (!match) return Option.none();
|
|
721
|
+
const bytes = Number(match[1]) * (SIZE_UNITS[match[2] ?? ""] ?? 1);
|
|
722
|
+
return Option.some(bound === "minSize" ? { minSize: bytes } : { maxSize: bytes });
|
|
723
|
+
};
|
|
724
|
+
//#endregion
|
|
725
|
+
//#region src/apis/JmapMail/index.ts
|
|
726
|
+
var JmapMail_exports = /* @__PURE__ */ __exportAll({
|
|
727
|
+
BodyValue: () => BodyValue,
|
|
728
|
+
EMAIL_PROPERTIES: () => EMAIL_PROPERTIES,
|
|
729
|
+
Email: () => Email,
|
|
730
|
+
EmailAddress: () => EmailAddress,
|
|
731
|
+
EmailBodyPart: () => EmailBodyPart,
|
|
732
|
+
EmailChangesResult: () => EmailChangesResult,
|
|
733
|
+
EmailGetResult: () => EmailGetResult,
|
|
734
|
+
EmailQueryResult: () => EmailQueryResult,
|
|
735
|
+
EmailSetResult: () => EmailSetResult,
|
|
736
|
+
EmailSubmissionSetResult: () => EmailSubmissionSetResult,
|
|
737
|
+
Identity: () => Identity,
|
|
738
|
+
IdentityGetResult: () => IdentityGetResult,
|
|
739
|
+
Mailbox: () => Mailbox,
|
|
740
|
+
MailboxGetResult: () => MailboxGetResult,
|
|
741
|
+
downloadBlob: () => downloadBlob,
|
|
742
|
+
emailChanges: () => emailChanges,
|
|
743
|
+
emailGet: () => emailGet,
|
|
744
|
+
emailQuery: () => emailQuery,
|
|
745
|
+
emailSetUpdate: () => emailSetUpdate,
|
|
746
|
+
filterScopesMailbox: () => filterScopesMailbox,
|
|
747
|
+
identityGet: () => identityGet,
|
|
748
|
+
mailboxGet: () => mailboxGet,
|
|
749
|
+
parseMailQuery: () => parseMailQuery,
|
|
750
|
+
resolveMailboxByNameOrRole: () => resolveMailboxByNameOrRole,
|
|
751
|
+
submitEmail: () => submitEmail
|
|
752
|
+
});
|
|
753
|
+
//#endregion
|
|
754
|
+
export { downloadBlob as a, emailQuery as c, mailboxGet as d, submitEmail as f, JmapCredentials as h, resolveMailboxByNameOrRole as i, emailSetUpdate as l, getSession as m, filterScopesMailbox as n, emailChanges as o, Jmap_exports as p, parseMailQuery as r, emailGet as s, JmapMail_exports as t, identityGet as u };
|
|
755
|
+
|
|
756
|
+
//# sourceMappingURL=chunk-apis.mjs.map
|