@upyo/jmap 0.6.0-dev.354 → 0.6.0-dev.357
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/dist/index.cjs +140 -1
- package/dist/index.d.cts +15 -2
- package/dist/index.d.ts +15 -2
- package/dist/index.js +140 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -143,7 +143,22 @@ var JmapHttpClient = class {
|
|
|
143
143
|
throw new JmapApiError(lastError.message, void 0, void 0, void 0, void 0, attempt + 1);
|
|
144
144
|
}
|
|
145
145
|
const delay = Math.pow(2, attempt) * 1e3;
|
|
146
|
-
await new Promise((resolve) =>
|
|
146
|
+
await new Promise((resolve, reject) => {
|
|
147
|
+
const cleanup = () => {
|
|
148
|
+
clearTimeout(timer);
|
|
149
|
+
signal?.removeEventListener("abort", onAbort);
|
|
150
|
+
};
|
|
151
|
+
const onAbort = () => {
|
|
152
|
+
cleanup();
|
|
153
|
+
reject(signal?.reason);
|
|
154
|
+
};
|
|
155
|
+
const timer = setTimeout(() => {
|
|
156
|
+
cleanup();
|
|
157
|
+
resolve();
|
|
158
|
+
}, delay);
|
|
159
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
160
|
+
if (signal?.aborted) onAbort();
|
|
161
|
+
});
|
|
147
162
|
}
|
|
148
163
|
}
|
|
149
164
|
if (lastError != null) throw lastError;
|
|
@@ -925,6 +940,60 @@ var JmapTransport = class {
|
|
|
925
940
|
this.httpClient = new JmapHttpClient(this.config);
|
|
926
941
|
}
|
|
927
942
|
/**
|
|
943
|
+
* Checks a fresh Session, the account used by send(), its drafts mailbox,
|
|
944
|
+
* and the configured or available identity without creating or sending mail.
|
|
945
|
+
* Does not read or update the session cache. Each discovery operation has
|
|
946
|
+
* config.timeout as its total budget, including response bodies and retries.
|
|
947
|
+
* Success does not guarantee acceptance of a particular sender or message.
|
|
948
|
+
* @param options Optional cancellation signal.
|
|
949
|
+
* @returns A promise that resolves when live verification succeeds.
|
|
950
|
+
* @throws {JmapApiError} If discovery, authentication, or validation fails.
|
|
951
|
+
* @throws The caller's abort reason if cancelled.
|
|
952
|
+
* @since 0.6.0
|
|
953
|
+
*/
|
|
954
|
+
async verify(options) {
|
|
955
|
+
const signal = options?.signal;
|
|
956
|
+
signal?.throwIfAborted();
|
|
957
|
+
try {
|
|
958
|
+
let session = await verificationOperation(this.config.timeout, (owned) => this.httpClient.fetchSession(owned), signal);
|
|
959
|
+
validateVerificationSession(session);
|
|
960
|
+
if (this.config.baseUrl) session = this.rewriteSessionUrls(session);
|
|
961
|
+
validateVerificationApiUrl(session.apiUrl);
|
|
962
|
+
if (!Object.values(JMAP_CAPABILITIES).every((cap) => cap in session.capabilities)) throw new JmapApiError("JMAP session lacks core, mail, or submission capability.");
|
|
963
|
+
const accountId = this.config.accountId ?? findMailAccount(session);
|
|
964
|
+
const account = accountId == null ? void 0 : session.accounts[accountId];
|
|
965
|
+
if (accountId == null || account == null) throw new JmapApiError(`No JMAP mail account found: ${accountId ?? "automatic selection"}`);
|
|
966
|
+
if (account.isReadOnly || !(JMAP_CAPABILITIES.mail in account.accountCapabilities) || !(JMAP_CAPABILITIES.submission in account.accountCapabilities)) throw new JmapApiError(`JMAP account must be writable with mail and submission capability: ${accountId}`);
|
|
967
|
+
const get = async (method, args, signal$1) => {
|
|
968
|
+
const response = await verificationOperation(this.config.timeout, (owned) => this.httpClient.executeRequest(session.apiUrl, {
|
|
969
|
+
using: method === "Mailbox/get" ? [JMAP_CAPABILITIES.core, JMAP_CAPABILITIES.mail] : [JMAP_CAPABILITIES.core, JMAP_CAPABILITIES.submission],
|
|
970
|
+
methodCalls: [[
|
|
971
|
+
method,
|
|
972
|
+
{
|
|
973
|
+
accountId,
|
|
974
|
+
...args
|
|
975
|
+
},
|
|
976
|
+
"verify"
|
|
977
|
+
]]
|
|
978
|
+
}, owned), signal$1);
|
|
979
|
+
return verificationResult(response, method, accountId);
|
|
980
|
+
};
|
|
981
|
+
const mailboxes = await get("Mailbox/get", { properties: ["id", "role"] }, signal);
|
|
982
|
+
if (!mailboxes.list.some((mailbox) => mailbox.role === "drafts")) throw new JmapApiError(`No drafts mailbox found in JMAP account: ${accountId}`);
|
|
983
|
+
const identityId = this.config.identityId;
|
|
984
|
+
const identities = await get("Identity/get", {
|
|
985
|
+
ids: identityId == null ? null : [identityId],
|
|
986
|
+
properties: ["id", "email"]
|
|
987
|
+
}, signal);
|
|
988
|
+
if (identities.notFound.length > 0 || identities.list.length === 0 || !identities.list.every((identity) => typeof identity.email === "string" && identity.email.length > 0) || identityId != null && (identities.list.length !== 1 || identities.list[0].id !== identityId)) throw new JmapApiError(`No valid JMAP identity found: ${identityId ?? accountId}`);
|
|
989
|
+
signal?.throwIfAborted();
|
|
990
|
+
} catch (error) {
|
|
991
|
+
signal?.throwIfAborted();
|
|
992
|
+
if (error instanceof JmapApiError) throw error;
|
|
993
|
+
throw new JmapApiError(`JMAP verification failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
928
997
|
* Sends a single email message.
|
|
929
998
|
* @param message The message to send.
|
|
930
999
|
* @param options Optional transport options.
|
|
@@ -1370,6 +1439,76 @@ function getAbortReason(signal, fallback) {
|
|
|
1370
1439
|
function isAbortError(error) {
|
|
1371
1440
|
return error instanceof Error && error.name === "AbortError";
|
|
1372
1441
|
}
|
|
1442
|
+
/** Bounds verification through body consumption, not just receipt of headers. */
|
|
1443
|
+
async function verificationOperation(timeout, operation, signal) {
|
|
1444
|
+
const controller = new AbortController();
|
|
1445
|
+
const combined = (0, __upyo_core.combineSignals)(controller.signal, signal);
|
|
1446
|
+
let onAbort;
|
|
1447
|
+
const timer = setTimeout(() => controller.abort(new JmapApiError("JMAP verification timed out.")), timeout);
|
|
1448
|
+
try {
|
|
1449
|
+
combined.signal.throwIfAborted();
|
|
1450
|
+
return await new Promise((resolve, reject) => {
|
|
1451
|
+
onAbort = () => reject(combined.signal.reason);
|
|
1452
|
+
combined.signal.addEventListener("abort", onAbort, { once: true });
|
|
1453
|
+
Promise.resolve().then(() => {
|
|
1454
|
+
combined.signal.throwIfAborted();
|
|
1455
|
+
return operation(combined.signal);
|
|
1456
|
+
}).then(resolve, reject);
|
|
1457
|
+
if (combined.signal.aborted) onAbort();
|
|
1458
|
+
});
|
|
1459
|
+
} finally {
|
|
1460
|
+
clearTimeout(timer);
|
|
1461
|
+
if (onAbort) combined.signal.removeEventListener("abort", onAbort);
|
|
1462
|
+
controller.abort();
|
|
1463
|
+
combined.cleanup();
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
function validateVerificationApiUrl(value) {
|
|
1467
|
+
if (typeof value === "string" && URL.canParse(value)) {
|
|
1468
|
+
const url = new URL(value);
|
|
1469
|
+
if (url.protocol === "https:" || url.protocol === "http:") return;
|
|
1470
|
+
}
|
|
1471
|
+
throw new JmapApiError("Invalid JMAP Session field: apiUrl");
|
|
1472
|
+
}
|
|
1473
|
+
function validateVerificationSession(session) {
|
|
1474
|
+
if (!isRecord(session)) throw new JmapApiError("Invalid JMAP Session response.");
|
|
1475
|
+
if (!isRecord(session.capabilities)) throw new JmapApiError("Invalid JMAP Session field: capabilities");
|
|
1476
|
+
if (!isRecord(session.accounts)) throw new JmapApiError("Invalid JMAP Session field: accounts");
|
|
1477
|
+
for (const [id, account] of Object.entries(session.accounts)) if (!isRecord(account) || typeof account.isReadOnly !== "boolean" || !isRecord(account.accountCapabilities)) throw new JmapApiError(`Invalid JMAP Session account: ${id}`);
|
|
1478
|
+
validateVerificationApiUrl(session.apiUrl);
|
|
1479
|
+
}
|
|
1480
|
+
/** Validates only the discovery fields used by verification. */
|
|
1481
|
+
function verificationResult(response, method, accountId) {
|
|
1482
|
+
if (!isRecord(response) || !Array.isArray(response.methodResponses)) throw new JmapApiError("Invalid JMAP verification response.");
|
|
1483
|
+
const matches = response.methodResponses.filter((entry$1) => Array.isArray(entry$1) && entry$1[2] === "verify");
|
|
1484
|
+
if (matches.length !== 1) throw new JmapApiError("Missing or duplicate JMAP verification response.");
|
|
1485
|
+
const entry = matches[0];
|
|
1486
|
+
if (!Array.isArray(entry) || entry.length !== 3 || !isRecord(entry[1])) throw new JmapApiError("Invalid JMAP verification method response.");
|
|
1487
|
+
const result = entry[1];
|
|
1488
|
+
if (entry[0] === "error") {
|
|
1489
|
+
if (typeof result.type !== "string" || !result.type) throw new JmapApiError("Invalid JMAP method error.");
|
|
1490
|
+
throw new JmapApiError(typeof result.description === "string" ? result.description : `JMAP method failed: ${result.type}`, void 0, void 0, result.type);
|
|
1491
|
+
}
|
|
1492
|
+
if (entry[0] !== method || result.accountId !== accountId || !Array.isArray(result.list) || !Array.isArray(result.notFound)) throw new JmapApiError(`Invalid JMAP discovery result: ${method}`);
|
|
1493
|
+
const list = [];
|
|
1494
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1495
|
+
for (const item of result.list) {
|
|
1496
|
+
if (!isRecord(item) || typeof item.id !== "string" || !item.id || ids.has(item.id)) throw new JmapApiError(`Invalid JMAP discovery identifiers: ${method}`);
|
|
1497
|
+
ids.add(item.id);
|
|
1498
|
+
list.push(item);
|
|
1499
|
+
}
|
|
1500
|
+
const notFound = [];
|
|
1501
|
+
for (const id of result.notFound) {
|
|
1502
|
+
if (typeof id !== "string" || !id || ids.has(id)) throw new JmapApiError(`Contradictory JMAP discovery identifiers: ${method}`);
|
|
1503
|
+
ids.add(id);
|
|
1504
|
+
notFound.push(id);
|
|
1505
|
+
}
|
|
1506
|
+
if (method === "Mailbox/get" && (notFound.length > 0 || !list.every((item) => item.role == null || typeof item.role === "string"))) throw new JmapApiError("Invalid JMAP mailbox discovery result.");
|
|
1507
|
+
return {
|
|
1508
|
+
list,
|
|
1509
|
+
notFound
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1373
1512
|
|
|
1374
1513
|
//#endregion
|
|
1375
1514
|
exports.JMAP_ERROR_TYPES = JMAP_ERROR_TYPES;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Message, RawMessage, RawTransport, Receipt, TransportOptions } from "@upyo/core";
|
|
1
|
+
import { Message, RawMessage, RawTransport, Receipt, TransportOptions, VerifiableTransport } from "@upyo/core";
|
|
2
2
|
|
|
3
3
|
//#region src/config.d.ts
|
|
4
4
|
|
|
@@ -103,7 +103,7 @@ declare function createJmapConfig(config: JmapConfig): ResolvedJmapConfig;
|
|
|
103
103
|
* JMAP transport for sending emails via JMAP protocol (RFC 8620/8621).
|
|
104
104
|
* @since 0.4.0
|
|
105
105
|
*/
|
|
106
|
-
declare class JmapTransport implements RawTransport<"jmap"> {
|
|
106
|
+
declare class JmapTransport implements RawTransport<"jmap">, VerifiableTransport<"jmap"> {
|
|
107
107
|
readonly id = "jmap";
|
|
108
108
|
readonly config: ResolvedJmapConfig;
|
|
109
109
|
private readonly httpClient;
|
|
@@ -114,6 +114,19 @@ declare class JmapTransport implements RawTransport<"jmap"> {
|
|
|
114
114
|
* @since 0.4.0
|
|
115
115
|
*/
|
|
116
116
|
constructor(config: JmapConfig);
|
|
117
|
+
/**
|
|
118
|
+
* Checks a fresh Session, the account used by send(), its drafts mailbox,
|
|
119
|
+
* and the configured or available identity without creating or sending mail.
|
|
120
|
+
* Does not read or update the session cache. Each discovery operation has
|
|
121
|
+
* config.timeout as its total budget, including response bodies and retries.
|
|
122
|
+
* Success does not guarantee acceptance of a particular sender or message.
|
|
123
|
+
* @param options Optional cancellation signal.
|
|
124
|
+
* @returns A promise that resolves when live verification succeeds.
|
|
125
|
+
* @throws {JmapApiError} If discovery, authentication, or validation fails.
|
|
126
|
+
* @throws The caller's abort reason if cancelled.
|
|
127
|
+
* @since 0.6.0
|
|
128
|
+
*/
|
|
129
|
+
verify(options?: TransportOptions): Promise<void>;
|
|
117
130
|
/**
|
|
118
131
|
* Sends a single email message.
|
|
119
132
|
* @param message The message to send.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Message, RawMessage, RawTransport, Receipt, TransportOptions } from "@upyo/core";
|
|
1
|
+
import { Message, RawMessage, RawTransport, Receipt, TransportOptions, VerifiableTransport } from "@upyo/core";
|
|
2
2
|
|
|
3
3
|
//#region src/config.d.ts
|
|
4
4
|
|
|
@@ -103,7 +103,7 @@ declare function createJmapConfig(config: JmapConfig): ResolvedJmapConfig;
|
|
|
103
103
|
* JMAP transport for sending emails via JMAP protocol (RFC 8620/8621).
|
|
104
104
|
* @since 0.4.0
|
|
105
105
|
*/
|
|
106
|
-
declare class JmapTransport implements RawTransport<"jmap"> {
|
|
106
|
+
declare class JmapTransport implements RawTransport<"jmap">, VerifiableTransport<"jmap"> {
|
|
107
107
|
readonly id = "jmap";
|
|
108
108
|
readonly config: ResolvedJmapConfig;
|
|
109
109
|
private readonly httpClient;
|
|
@@ -114,6 +114,19 @@ declare class JmapTransport implements RawTransport<"jmap"> {
|
|
|
114
114
|
* @since 0.4.0
|
|
115
115
|
*/
|
|
116
116
|
constructor(config: JmapConfig);
|
|
117
|
+
/**
|
|
118
|
+
* Checks a fresh Session, the account used by send(), its drafts mailbox,
|
|
119
|
+
* and the configured or available identity without creating or sending mail.
|
|
120
|
+
* Does not read or update the session cache. Each discovery operation has
|
|
121
|
+
* config.timeout as its total budget, including response bodies and retries.
|
|
122
|
+
* Success does not guarantee acceptance of a particular sender or message.
|
|
123
|
+
* @param options Optional cancellation signal.
|
|
124
|
+
* @returns A promise that resolves when live verification succeeds.
|
|
125
|
+
* @throws {JmapApiError} If discovery, authentication, or validation fails.
|
|
126
|
+
* @throws The caller's abort reason if cancelled.
|
|
127
|
+
* @since 0.6.0
|
|
128
|
+
*/
|
|
129
|
+
verify(options?: TransportOptions): Promise<void>;
|
|
117
130
|
/**
|
|
118
131
|
* Sends a single email message.
|
|
119
132
|
* @param message The message to send.
|
package/dist/index.js
CHANGED
|
@@ -120,7 +120,22 @@ var JmapHttpClient = class {
|
|
|
120
120
|
throw new JmapApiError(lastError.message, void 0, void 0, void 0, void 0, attempt + 1);
|
|
121
121
|
}
|
|
122
122
|
const delay = Math.pow(2, attempt) * 1e3;
|
|
123
|
-
await new Promise((resolve) =>
|
|
123
|
+
await new Promise((resolve, reject) => {
|
|
124
|
+
const cleanup = () => {
|
|
125
|
+
clearTimeout(timer);
|
|
126
|
+
signal?.removeEventListener("abort", onAbort);
|
|
127
|
+
};
|
|
128
|
+
const onAbort = () => {
|
|
129
|
+
cleanup();
|
|
130
|
+
reject(signal?.reason);
|
|
131
|
+
};
|
|
132
|
+
const timer = setTimeout(() => {
|
|
133
|
+
cleanup();
|
|
134
|
+
resolve();
|
|
135
|
+
}, delay);
|
|
136
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
137
|
+
if (signal?.aborted) onAbort();
|
|
138
|
+
});
|
|
124
139
|
}
|
|
125
140
|
}
|
|
126
141
|
if (lastError != null) throw lastError;
|
|
@@ -902,6 +917,60 @@ var JmapTransport = class {
|
|
|
902
917
|
this.httpClient = new JmapHttpClient(this.config);
|
|
903
918
|
}
|
|
904
919
|
/**
|
|
920
|
+
* Checks a fresh Session, the account used by send(), its drafts mailbox,
|
|
921
|
+
* and the configured or available identity without creating or sending mail.
|
|
922
|
+
* Does not read or update the session cache. Each discovery operation has
|
|
923
|
+
* config.timeout as its total budget, including response bodies and retries.
|
|
924
|
+
* Success does not guarantee acceptance of a particular sender or message.
|
|
925
|
+
* @param options Optional cancellation signal.
|
|
926
|
+
* @returns A promise that resolves when live verification succeeds.
|
|
927
|
+
* @throws {JmapApiError} If discovery, authentication, or validation fails.
|
|
928
|
+
* @throws The caller's abort reason if cancelled.
|
|
929
|
+
* @since 0.6.0
|
|
930
|
+
*/
|
|
931
|
+
async verify(options) {
|
|
932
|
+
const signal = options?.signal;
|
|
933
|
+
signal?.throwIfAborted();
|
|
934
|
+
try {
|
|
935
|
+
let session = await verificationOperation(this.config.timeout, (owned) => this.httpClient.fetchSession(owned), signal);
|
|
936
|
+
validateVerificationSession(session);
|
|
937
|
+
if (this.config.baseUrl) session = this.rewriteSessionUrls(session);
|
|
938
|
+
validateVerificationApiUrl(session.apiUrl);
|
|
939
|
+
if (!Object.values(JMAP_CAPABILITIES).every((cap) => cap in session.capabilities)) throw new JmapApiError("JMAP session lacks core, mail, or submission capability.");
|
|
940
|
+
const accountId = this.config.accountId ?? findMailAccount(session);
|
|
941
|
+
const account = accountId == null ? void 0 : session.accounts[accountId];
|
|
942
|
+
if (accountId == null || account == null) throw new JmapApiError(`No JMAP mail account found: ${accountId ?? "automatic selection"}`);
|
|
943
|
+
if (account.isReadOnly || !(JMAP_CAPABILITIES.mail in account.accountCapabilities) || !(JMAP_CAPABILITIES.submission in account.accountCapabilities)) throw new JmapApiError(`JMAP account must be writable with mail and submission capability: ${accountId}`);
|
|
944
|
+
const get = async (method, args, signal$1) => {
|
|
945
|
+
const response = await verificationOperation(this.config.timeout, (owned) => this.httpClient.executeRequest(session.apiUrl, {
|
|
946
|
+
using: method === "Mailbox/get" ? [JMAP_CAPABILITIES.core, JMAP_CAPABILITIES.mail] : [JMAP_CAPABILITIES.core, JMAP_CAPABILITIES.submission],
|
|
947
|
+
methodCalls: [[
|
|
948
|
+
method,
|
|
949
|
+
{
|
|
950
|
+
accountId,
|
|
951
|
+
...args
|
|
952
|
+
},
|
|
953
|
+
"verify"
|
|
954
|
+
]]
|
|
955
|
+
}, owned), signal$1);
|
|
956
|
+
return verificationResult(response, method, accountId);
|
|
957
|
+
};
|
|
958
|
+
const mailboxes = await get("Mailbox/get", { properties: ["id", "role"] }, signal);
|
|
959
|
+
if (!mailboxes.list.some((mailbox) => mailbox.role === "drafts")) throw new JmapApiError(`No drafts mailbox found in JMAP account: ${accountId}`);
|
|
960
|
+
const identityId = this.config.identityId;
|
|
961
|
+
const identities = await get("Identity/get", {
|
|
962
|
+
ids: identityId == null ? null : [identityId],
|
|
963
|
+
properties: ["id", "email"]
|
|
964
|
+
}, signal);
|
|
965
|
+
if (identities.notFound.length > 0 || identities.list.length === 0 || !identities.list.every((identity) => typeof identity.email === "string" && identity.email.length > 0) || identityId != null && (identities.list.length !== 1 || identities.list[0].id !== identityId)) throw new JmapApiError(`No valid JMAP identity found: ${identityId ?? accountId}`);
|
|
966
|
+
signal?.throwIfAborted();
|
|
967
|
+
} catch (error) {
|
|
968
|
+
signal?.throwIfAborted();
|
|
969
|
+
if (error instanceof JmapApiError) throw error;
|
|
970
|
+
throw new JmapApiError(`JMAP verification failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
905
974
|
* Sends a single email message.
|
|
906
975
|
* @param message The message to send.
|
|
907
976
|
* @param options Optional transport options.
|
|
@@ -1347,6 +1416,76 @@ function getAbortReason(signal, fallback) {
|
|
|
1347
1416
|
function isAbortError(error) {
|
|
1348
1417
|
return error instanceof Error && error.name === "AbortError";
|
|
1349
1418
|
}
|
|
1419
|
+
/** Bounds verification through body consumption, not just receipt of headers. */
|
|
1420
|
+
async function verificationOperation(timeout, operation, signal) {
|
|
1421
|
+
const controller = new AbortController();
|
|
1422
|
+
const combined = combineSignals(controller.signal, signal);
|
|
1423
|
+
let onAbort;
|
|
1424
|
+
const timer = setTimeout(() => controller.abort(new JmapApiError("JMAP verification timed out.")), timeout);
|
|
1425
|
+
try {
|
|
1426
|
+
combined.signal.throwIfAborted();
|
|
1427
|
+
return await new Promise((resolve, reject) => {
|
|
1428
|
+
onAbort = () => reject(combined.signal.reason);
|
|
1429
|
+
combined.signal.addEventListener("abort", onAbort, { once: true });
|
|
1430
|
+
Promise.resolve().then(() => {
|
|
1431
|
+
combined.signal.throwIfAborted();
|
|
1432
|
+
return operation(combined.signal);
|
|
1433
|
+
}).then(resolve, reject);
|
|
1434
|
+
if (combined.signal.aborted) onAbort();
|
|
1435
|
+
});
|
|
1436
|
+
} finally {
|
|
1437
|
+
clearTimeout(timer);
|
|
1438
|
+
if (onAbort) combined.signal.removeEventListener("abort", onAbort);
|
|
1439
|
+
controller.abort();
|
|
1440
|
+
combined.cleanup();
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
function validateVerificationApiUrl(value) {
|
|
1444
|
+
if (typeof value === "string" && URL.canParse(value)) {
|
|
1445
|
+
const url = new URL(value);
|
|
1446
|
+
if (url.protocol === "https:" || url.protocol === "http:") return;
|
|
1447
|
+
}
|
|
1448
|
+
throw new JmapApiError("Invalid JMAP Session field: apiUrl");
|
|
1449
|
+
}
|
|
1450
|
+
function validateVerificationSession(session) {
|
|
1451
|
+
if (!isRecord(session)) throw new JmapApiError("Invalid JMAP Session response.");
|
|
1452
|
+
if (!isRecord(session.capabilities)) throw new JmapApiError("Invalid JMAP Session field: capabilities");
|
|
1453
|
+
if (!isRecord(session.accounts)) throw new JmapApiError("Invalid JMAP Session field: accounts");
|
|
1454
|
+
for (const [id, account] of Object.entries(session.accounts)) if (!isRecord(account) || typeof account.isReadOnly !== "boolean" || !isRecord(account.accountCapabilities)) throw new JmapApiError(`Invalid JMAP Session account: ${id}`);
|
|
1455
|
+
validateVerificationApiUrl(session.apiUrl);
|
|
1456
|
+
}
|
|
1457
|
+
/** Validates only the discovery fields used by verification. */
|
|
1458
|
+
function verificationResult(response, method, accountId) {
|
|
1459
|
+
if (!isRecord(response) || !Array.isArray(response.methodResponses)) throw new JmapApiError("Invalid JMAP verification response.");
|
|
1460
|
+
const matches = response.methodResponses.filter((entry$1) => Array.isArray(entry$1) && entry$1[2] === "verify");
|
|
1461
|
+
if (matches.length !== 1) throw new JmapApiError("Missing or duplicate JMAP verification response.");
|
|
1462
|
+
const entry = matches[0];
|
|
1463
|
+
if (!Array.isArray(entry) || entry.length !== 3 || !isRecord(entry[1])) throw new JmapApiError("Invalid JMAP verification method response.");
|
|
1464
|
+
const result = entry[1];
|
|
1465
|
+
if (entry[0] === "error") {
|
|
1466
|
+
if (typeof result.type !== "string" || !result.type) throw new JmapApiError("Invalid JMAP method error.");
|
|
1467
|
+
throw new JmapApiError(typeof result.description === "string" ? result.description : `JMAP method failed: ${result.type}`, void 0, void 0, result.type);
|
|
1468
|
+
}
|
|
1469
|
+
if (entry[0] !== method || result.accountId !== accountId || !Array.isArray(result.list) || !Array.isArray(result.notFound)) throw new JmapApiError(`Invalid JMAP discovery result: ${method}`);
|
|
1470
|
+
const list = [];
|
|
1471
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1472
|
+
for (const item of result.list) {
|
|
1473
|
+
if (!isRecord(item) || typeof item.id !== "string" || !item.id || ids.has(item.id)) throw new JmapApiError(`Invalid JMAP discovery identifiers: ${method}`);
|
|
1474
|
+
ids.add(item.id);
|
|
1475
|
+
list.push(item);
|
|
1476
|
+
}
|
|
1477
|
+
const notFound = [];
|
|
1478
|
+
for (const id of result.notFound) {
|
|
1479
|
+
if (typeof id !== "string" || !id || ids.has(id)) throw new JmapApiError(`Contradictory JMAP discovery identifiers: ${method}`);
|
|
1480
|
+
ids.add(id);
|
|
1481
|
+
notFound.push(id);
|
|
1482
|
+
}
|
|
1483
|
+
if (method === "Mailbox/get" && (notFound.length > 0 || !list.every((item) => item.role == null || typeof item.role === "string"))) throw new JmapApiError("Invalid JMAP mailbox discovery result.");
|
|
1484
|
+
return {
|
|
1485
|
+
list,
|
|
1486
|
+
notFound
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1350
1489
|
|
|
1351
1490
|
//#endregion
|
|
1352
1491
|
export { JMAP_ERROR_TYPES, JmapApiError, JmapTransport, createJmapConfig, isCapabilityError, uploadBlob };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@upyo/jmap",
|
|
3
|
-
"version": "0.6.0-dev.
|
|
3
|
+
"version": "0.6.0-dev.357",
|
|
4
4
|
"description": "JMAP transport for Upyo email library",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"email",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
},
|
|
54
54
|
"sideEffects": false,
|
|
55
55
|
"peerDependencies": {
|
|
56
|
-
"@upyo/core": "0.6.0-dev.
|
|
56
|
+
"@upyo/core": "0.6.0-dev.357+6aaf02d1"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"jmap-rfc-types": "^0.1.2",
|