@wtfalch/email 0.3.0 → 0.4.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/dist/mailbox/client.d.ts +5 -7
- package/dist/mailbox/client.js +47 -40
- package/dist/mailbox/identities.d.ts +1 -1
- package/dist/mailbox/identities.js +2 -1
- package/dist/mailbox/mailboxes.d.ts +1 -1
- package/dist/mailbox/mailboxes.js +2 -1
- package/dist/mailbox/submit.d.ts +1 -1
- package/dist/mailbox/submit.js +3 -2
- package/dist/mailbox/thread.d.ts +1 -1
- package/dist/mailbox/thread.js +2 -1
- package/dist/mailbox/threads.d.ts +1 -1
- package/dist/mailbox/threads.js +2 -1
- package/package.json +11 -20
- package/LICENSE +0 -21
package/dist/mailbox/client.d.ts
CHANGED
|
@@ -31,13 +31,11 @@ export type MailClientOptions = {
|
|
|
31
31
|
/** Use a different account than the session's primary one. */
|
|
32
32
|
accountId?: string;
|
|
33
33
|
};
|
|
34
|
-
/**
|
|
35
|
-
*
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
* through; `jmap-jam` itself is not part of this package's public surface.
|
|
40
|
-
*/
|
|
34
|
+
/** Passed to every `jmap-jam` call, whose `fetchInit` is spread over the
|
|
35
|
+
* request it builds. */
|
|
36
|
+
export declare const JAM_FETCH: {
|
|
37
|
+
fetchInit: RequestInit;
|
|
38
|
+
};
|
|
41
39
|
export declare class MailClient {
|
|
42
40
|
#private;
|
|
43
41
|
constructor(options: MailClientOptions);
|
package/dist/mailbox/client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { JamClient } from 'jmap-jam';
|
|
2
|
-
import { MailError, guard
|
|
2
|
+
import { MailError, guard } from "./errors.js";
|
|
3
3
|
import { expandTemplate } from "./uri.js";
|
|
4
4
|
export const MAIL_CAPABILITY = 'urn:ietf:params:jmap:mail';
|
|
5
5
|
export const SUBMISSION_CAPABILITY = 'urn:ietf:params:jmap:submission';
|
|
@@ -16,6 +16,26 @@ function looksLikeSession(value) {
|
|
|
16
16
|
* document, the account id, and the `jmap-jam` client the wire calls go
|
|
17
17
|
* through; `jmap-jam` itself is not part of this package's public surface.
|
|
18
18
|
*/
|
|
19
|
+
/**
|
|
20
|
+
* Never let the browser answer an authentication challenge for us.
|
|
21
|
+
*
|
|
22
|
+
* Every request here carries its own `Authorization` header. If one is
|
|
23
|
+
* refused, Stalwart answers with both `WWW-Authenticate: Bearer` and
|
|
24
|
+
* `WWW-Authenticate: Basic`, and a browser that is allowed to use
|
|
25
|
+
* credentials honours the second by opening its native password prompt —
|
|
26
|
+
* a modal that hides whatever the application was about to say about the
|
|
27
|
+
* refusal. `omit` declines that on our behalf: the 401 comes back as an
|
|
28
|
+
* ordinary response and the application gets to explain it.
|
|
29
|
+
*
|
|
30
|
+
* Outside a browser this is inert.
|
|
31
|
+
*/
|
|
32
|
+
const NO_BROWSER_CREDENTIALS = { credentials: 'omit' };
|
|
33
|
+
/** Passed to every `jmap-jam` call, whose `fetchInit` is spread over the
|
|
34
|
+
* request it builds. */
|
|
35
|
+
export const JAM_FETCH = { fetchInit: NO_BROWSER_CREDENTIALS };
|
|
36
|
+
/** Handed to `JamClient`'s constructor so its eager session request resolves
|
|
37
|
+
* in-process instead of going to the server. See `#open`. */
|
|
38
|
+
const NO_SESSION_URL = 'data:application/json,{}';
|
|
19
39
|
export class MailClient {
|
|
20
40
|
#sessionUrl;
|
|
21
41
|
#wantedAccountId;
|
|
@@ -60,37 +80,34 @@ export class MailClient {
|
|
|
60
80
|
async #open() {
|
|
61
81
|
const authorization = this.#authorization;
|
|
62
82
|
const isBearer = authorization.startsWith('Bearer ');
|
|
63
|
-
// `JamClient`
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
83
|
+
// `JamClient`'s constructor fires a session request of its own, and we
|
|
84
|
+
// want neither the request nor its result. It calls `.json()` without
|
|
85
|
+
// looking at the status, so a refusal arrives as an unreadable parse
|
|
86
|
+
// error — and, worse, it is a plain same-origin fetch, so a 401 carrying
|
|
87
|
+
// `WWW-Authenticate: Basic` makes the browser put up its own password
|
|
88
|
+
// prompt. To a person that is indistinguishable from being asked to sign
|
|
89
|
+
// in again, on the right domain, when in fact they are already signed in
|
|
90
|
+
// and the token is the problem.
|
|
91
|
+
//
|
|
92
|
+
// So point the constructor at a `data:` URL, which resolves in-process
|
|
93
|
+
// and never touches the network, and read the session ourselves with the
|
|
94
|
+
// status checked. `jam.session` is overwritten with that document, and
|
|
95
|
+
// it is the document — not the URL — that carries `apiUrl`, so nothing
|
|
96
|
+
// downstream notices.
|
|
70
97
|
const jam = new JamClient({
|
|
71
|
-
sessionUrl:
|
|
98
|
+
sessionUrl: NO_SESSION_URL,
|
|
72
99
|
bearerToken: isBearer ? authorization.slice('Bearer '.length) : '',
|
|
73
100
|
});
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
// `JamClient.loadSession` calls `.json()` without checking the
|
|
82
|
-
// status, so a 401 with an HTML body arrives here as a parse error.
|
|
83
|
-
throw await this.#sessionFailure(authorization, cause);
|
|
84
|
-
}
|
|
85
|
-
if (!looksLikeSession(loaded))
|
|
86
|
-
throw await this.#sessionFailure(authorization, loaded);
|
|
87
|
-
doc = loaded;
|
|
88
|
-
}
|
|
89
|
-
else {
|
|
101
|
+
// Nothing ever reads the promise the constructor made, and in Node an
|
|
102
|
+
// unobserved rejection takes the process down. Say so explicitly.
|
|
103
|
+
void jam.session.catch(() => { });
|
|
104
|
+
// `JamClient` can only build a `Bearer` header from the string it is
|
|
105
|
+
// given, so a credential that is not a bearer token — an app password,
|
|
106
|
+
// which Stalwart takes as Basic — needs the header replaced.
|
|
107
|
+
if (!isBearer)
|
|
90
108
|
jam.authHeader = authorization;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
109
|
+
const doc = await this.#fetchSession(authorization);
|
|
110
|
+
jam.session = Promise.resolve(doc);
|
|
94
111
|
const accountId = this.#wantedAccountId ?? doc.primaryAccounts?.[MAIL_CAPABILITY];
|
|
95
112
|
if (!accountId) {
|
|
96
113
|
throw new MailError(`the session document names no primary account for ${MAIL_CAPABILITY}; the server may not offer JMAP Mail to this login`, { operation: 'client.connect' });
|
|
@@ -110,6 +127,7 @@ export class MailClient {
|
|
|
110
127
|
response = await fetch(this.#sessionUrl, {
|
|
111
128
|
headers: { Authorization: authorization, Accept: 'application/json' },
|
|
112
129
|
cache: 'no-cache',
|
|
130
|
+
...NO_BROWSER_CREDENTIALS,
|
|
113
131
|
});
|
|
114
132
|
}
|
|
115
133
|
catch (cause) {
|
|
@@ -134,18 +152,6 @@ export class MailClient {
|
|
|
134
152
|
}
|
|
135
153
|
return parsed;
|
|
136
154
|
}
|
|
137
|
-
/** Turn a failed or unrecognisable session read into an error that says
|
|
138
|
-
* what happened, by asking again with the status checked. */
|
|
139
|
-
async #sessionFailure(authorization, cause) {
|
|
140
|
-
try {
|
|
141
|
-
await this.#fetchSession(authorization);
|
|
142
|
-
}
|
|
143
|
-
catch (error) {
|
|
144
|
-
return toMailError(error, 'client.connect');
|
|
145
|
-
}
|
|
146
|
-
// The second read succeeded, so the first was a transient failure.
|
|
147
|
-
return toMailError(cause, 'client.connect');
|
|
148
|
-
}
|
|
149
155
|
/** The signed-in person and the account being read. */
|
|
150
156
|
async session() {
|
|
151
157
|
const { doc, accountId } = await this.connect();
|
|
@@ -185,6 +191,7 @@ export class MailClient {
|
|
|
185
191
|
return guard('client.upload', async () => {
|
|
186
192
|
const result = await jam.uploadBlob(accountId, body, {
|
|
187
193
|
headers: { Authorization: this.#authorization, 'Content-Type': type },
|
|
194
|
+
...NO_BROWSER_CREDENTIALS,
|
|
188
195
|
});
|
|
189
196
|
return { blobId: result.blobId, type: result.type, size: result.size };
|
|
190
197
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type MailClient } from './client.ts';
|
|
2
2
|
import type { MailIdentity } from './types.ts';
|
|
3
3
|
/** Every address this account may send as. */
|
|
4
4
|
export declare function identities(client: MailClient): Promise<readonly MailIdentity[]>;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { JAM_FETCH } from "./client.js";
|
|
1
2
|
import { guard } from "./errors.js";
|
|
2
3
|
/** Every address this account may send as. */
|
|
3
4
|
export async function identities(client) {
|
|
4
5
|
const { jam, accountId } = await client.connect();
|
|
5
6
|
return guard('identities', async () => {
|
|
6
|
-
const [result] = await jam.api.Identity.get({ accountId });
|
|
7
|
+
const [result] = await jam.api.Identity.get({ accountId }, JAM_FETCH);
|
|
7
8
|
return result.list.map((identity) => ({
|
|
8
9
|
id: identity.id,
|
|
9
10
|
name: identity.name ?? '',
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { JAM_FETCH } from "./client.js";
|
|
1
2
|
import { guard } from "./errors.js";
|
|
2
3
|
/** RFC 8621 §2: order by `sortOrder`, and break ties by name. */
|
|
3
4
|
function compare(a, b) {
|
|
@@ -101,7 +102,7 @@ export async function mailboxes(client) {
|
|
|
101
102
|
const [result] = await jam.api.Mailbox.get({
|
|
102
103
|
accountId,
|
|
103
104
|
properties: MAILBOX_PROPERTIES,
|
|
104
|
-
});
|
|
105
|
+
}, JAM_FETCH);
|
|
105
106
|
return buildTree(result.list);
|
|
106
107
|
});
|
|
107
108
|
}
|
package/dist/mailbox/submit.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type MailClient } from './client.ts';
|
|
2
2
|
import type { Draft, EmailAddress, MailIdentity, MailboxNode, Sent } from './types.ts';
|
|
3
3
|
/** The JMAP `Email` object a draft becomes. Exported so the shape can be
|
|
4
4
|
* asserted without a server. */
|
package/dist/mailbox/submit.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { JAM_FETCH } from "./client.js";
|
|
1
2
|
import { MailError, guard } from "./errors.js";
|
|
2
3
|
import { identities } from "./identities.js";
|
|
3
4
|
import { findRole, mailboxes } from "./mailboxes.js";
|
|
@@ -119,7 +120,7 @@ export async function send(client, draft, options = {}) {
|
|
|
119
120
|
const [created] = await jam.api.Email.set({
|
|
120
121
|
accountId,
|
|
121
122
|
create: { draft: draftToEmail(draft, from, drafts.id) },
|
|
122
|
-
});
|
|
123
|
+
}, JAM_FETCH);
|
|
123
124
|
const email = created.created?.draft;
|
|
124
125
|
if (!email)
|
|
125
126
|
failed('draft', created.notCreated);
|
|
@@ -137,7 +138,7 @@ export async function send(client, draft, options = {}) {
|
|
|
137
138
|
// declare the mail capability as well as submission. jmap-jam derives
|
|
138
139
|
// capabilities from the method names alone and would send only
|
|
139
140
|
// submission and core.
|
|
140
|
-
{ using: ['urn:ietf:params:jmap:mail'] });
|
|
141
|
+
{ using: ['urn:ietf:params:jmap:mail'], ...JAM_FETCH });
|
|
141
142
|
const submission = submitted.created?.submission;
|
|
142
143
|
if (!submission)
|
|
143
144
|
failed('submission', submitted.notCreated);
|
package/dist/mailbox/thread.d.ts
CHANGED
package/dist/mailbox/thread.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { JAM_FETCH } from "./client.js";
|
|
1
2
|
import { MailError, guard } from "./errors.js";
|
|
2
3
|
import { expandTemplate } from "./uri.js";
|
|
3
4
|
const MESSAGE_PROPERTIES = [
|
|
@@ -125,7 +126,7 @@ export async function thread(client, threadId, options = {}) {
|
|
|
125
126
|
maxBodyValueBytes: options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES,
|
|
126
127
|
});
|
|
127
128
|
return { threads, messages };
|
|
128
|
-
});
|
|
129
|
+
}, JAM_FETCH);
|
|
129
130
|
const found = results.threads.list;
|
|
130
131
|
if (found.length === 0) {
|
|
131
132
|
throw new MailError(`no thread ${threadId} in this account`, {
|
package/dist/mailbox/threads.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { JAM_FETCH } from "./client.js";
|
|
1
2
|
import { guard } from "./errors.js";
|
|
2
3
|
/** What a list row needs from the newest message in each thread. */
|
|
3
4
|
const SUMMARY_PROPERTIES = [
|
|
@@ -121,7 +122,7 @@ export async function queryThreads(client, filter, options = {}) {
|
|
|
121
122
|
properties: MEMBER_PROPERTIES,
|
|
122
123
|
});
|
|
123
124
|
return { query, latest, threads, members };
|
|
124
|
-
});
|
|
125
|
+
}, JAM_FETCH);
|
|
125
126
|
const query = results.query;
|
|
126
127
|
const latest = results.latest;
|
|
127
128
|
const threads = results.threads;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wtfalch/email",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "The wtfalch estate: reading a mailbox over JMAP, and administering the Stalwart server it lives on. Two entries with no code in common.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -33,11 +33,14 @@
|
|
|
33
33
|
},
|
|
34
34
|
"./package.json": "./package.json"
|
|
35
35
|
},
|
|
36
|
-
"files": [
|
|
37
|
-
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
|
|
36
|
+
"files": ["dist", "LICENSE", "README.md"],
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json && cp src/mailbox/react/mail.css dist/mailbox/mail.css",
|
|
39
|
+
"prepack": "pnpm build",
|
|
40
|
+
"typecheck": "tsc --noEmit",
|
|
41
|
+
"test": "vitest run",
|
|
42
|
+
"record-fixtures": "node scripts/record-fixtures.mjs"
|
|
43
|
+
},
|
|
41
44
|
"peerDependencies": {
|
|
42
45
|
"@wtfalch/design": ">=0.4.0",
|
|
43
46
|
"react": "^19.0.0",
|
|
@@ -72,20 +75,8 @@
|
|
|
72
75
|
"engines": {
|
|
73
76
|
"node": ">=22.0.0"
|
|
74
77
|
},
|
|
75
|
-
"keywords": [
|
|
76
|
-
"jmap",
|
|
77
|
-
"email",
|
|
78
|
-
"mail",
|
|
79
|
-
"stalwart",
|
|
80
|
-
"rfc8621"
|
|
81
|
-
],
|
|
78
|
+
"keywords": ["jmap", "email", "mail", "stalwart", "rfc8621"],
|
|
82
79
|
"dependencies": {
|
|
83
80
|
"jmap-jam": "^0.13.6"
|
|
84
|
-
},
|
|
85
|
-
"scripts": {
|
|
86
|
-
"build": "rm -rf dist && tsc -p tsconfig.build.json && cp src/mailbox/react/mail.css dist/mailbox/mail.css",
|
|
87
|
-
"typecheck": "tsc --noEmit",
|
|
88
|
-
"test": "vitest run",
|
|
89
|
-
"record-fixtures": "node scripts/record-fixtures.mjs"
|
|
90
81
|
}
|
|
91
|
-
}
|
|
82
|
+
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 William Tallis Falch
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|