@wtfalch/email 0.1.1 → 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/react/Mail.d.ts +45 -1
- package/dist/mailbox/react/Mail.js +36 -10
- package/dist/mailbox/react/index.d.ts +1 -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
|
}
|
|
@@ -1,8 +1,52 @@
|
|
|
1
1
|
import type { MailClient } from '../client.ts';
|
|
2
|
+
import type { MailError } from '../errors.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Where the reader is: which mailbox, which conversation.
|
|
5
|
+
*
|
|
6
|
+
* Its own type because an application that puts this in the URL has to be
|
|
7
|
+
* able to name it.
|
|
8
|
+
*/
|
|
9
|
+
export type MailLocation = {
|
|
10
|
+
mailboxId: string | null;
|
|
11
|
+
threadId: string | null;
|
|
12
|
+
};
|
|
2
13
|
export type MailProps = {
|
|
3
14
|
client: MailClient;
|
|
15
|
+
/**
|
|
16
|
+
* Drive the selection from outside — from the URL, almost always.
|
|
17
|
+
*
|
|
18
|
+
* Omit it and the component keeps its own, which is right for a gallery or
|
|
19
|
+
* an embed. Supply it with `onNavigate` and the two selections become the
|
|
20
|
+
* application's: `/inbox` and `/thread/abc` can be real addresses that
|
|
21
|
+
* survive a reload, appear in history and can be sent to someone.
|
|
22
|
+
*
|
|
23
|
+
* A partially controlled component is a trap, so this is all or nothing:
|
|
24
|
+
* pass `location` and you own both fields.
|
|
25
|
+
*/
|
|
26
|
+
location?: MailLocation;
|
|
27
|
+
/** Called when something in here wants to move. Required with `location`,
|
|
28
|
+
* because without it the controlled selection could never change. */
|
|
29
|
+
onNavigate?: (to: MailLocation) => void;
|
|
30
|
+
/**
|
|
31
|
+
* The right-hand end of the header: who is signed in, and the way out.
|
|
32
|
+
*
|
|
33
|
+
* A slot rather than anything this component builds, because `Mail` does
|
|
34
|
+
* not know how the person got here. It has no idea whether there is a
|
|
35
|
+
* session, a token, an issuer or a sign-out — and a mail view that grew
|
|
36
|
+
* opinions about authentication would be a mail view nobody could embed.
|
|
37
|
+
*/
|
|
38
|
+
account?: React.ReactNode;
|
|
39
|
+
/**
|
|
40
|
+
* Called when any of the fetches behind this view fails.
|
|
41
|
+
*
|
|
42
|
+
* The views already say "could not load" for themselves; this is for the
|
|
43
|
+
* application to act on a *class* of failure it alone can answer. The one
|
|
44
|
+
* that matters is 401: the token expired mid-session, and only the thing
|
|
45
|
+
* that obtained it can get another.
|
|
46
|
+
*/
|
|
47
|
+
onError?: (error: MailError) => void;
|
|
4
48
|
/** Now, for the list's relative times. Passed in so stories hold still. */
|
|
5
49
|
now?: Date;
|
|
6
50
|
className?: string;
|
|
7
51
|
};
|
|
8
|
-
export declare function Mail({ client, now, className }: MailProps): import("react").JSX.Element;
|
|
52
|
+
export declare function Mail({ client, location, onNavigate, account, onError, now, className, }: MailProps): import("react").JSX.Element;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Button, Command, Icon, Input, Modal, SplitPane } from '@wtfalch/design';
|
|
3
|
-
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
4
|
import { forwardDraft, replyDraft } from "../drafts.js";
|
|
5
5
|
import { findRole } from "../mailboxes.js";
|
|
6
6
|
import { send } from "../submit.js";
|
|
@@ -27,9 +27,25 @@ import { useIdentities, useMailboxes, usePush, useThread, useThreads } from "./h
|
|
|
27
27
|
* mailboxes as "go to" entries, so moving around never requires the mouse.
|
|
28
28
|
*/
|
|
29
29
|
const PAGE = 50;
|
|
30
|
-
export function Mail({ client, now, className }) {
|
|
31
|
-
const [
|
|
32
|
-
const [
|
|
30
|
+
export function Mail({ client, location, onNavigate, account, onError, now, className, }) {
|
|
31
|
+
const [ownMailboxId, setOwnMailboxId] = useState(null);
|
|
32
|
+
const [ownThreadId, setOwnThreadId] = useState(null);
|
|
33
|
+
/* Controlled when a location was given, and then entirely: reading one
|
|
34
|
+
field from the caller and the other from here is how a component ends up
|
|
35
|
+
with two disagreeing ideas of where it is. */
|
|
36
|
+
const controlled = location !== undefined;
|
|
37
|
+
const mailboxId = controlled ? location.mailboxId : ownMailboxId;
|
|
38
|
+
const threadId = controlled ? location.threadId : ownThreadId;
|
|
39
|
+
const navigate = useCallback((to) => {
|
|
40
|
+
if (controlled)
|
|
41
|
+
onNavigate?.(to);
|
|
42
|
+
else {
|
|
43
|
+
setOwnMailboxId(to.mailboxId);
|
|
44
|
+
setOwnThreadId(to.threadId);
|
|
45
|
+
}
|
|
46
|
+
}, [controlled, onNavigate]);
|
|
47
|
+
const setMailboxId = useCallback((next) => navigate({ mailboxId: next, threadId: null }), [navigate]);
|
|
48
|
+
const setThreadId = useCallback((next) => navigate({ mailboxId, threadId: next }), [navigate, mailboxId]);
|
|
33
49
|
const [position, setPosition] = useState(0);
|
|
34
50
|
const [query, setQuery] = useState('');
|
|
35
51
|
const [text, setText] = useState('');
|
|
@@ -48,7 +64,7 @@ export function Mail({ client, now, className }) {
|
|
|
48
64
|
const inbox = findRole(boxes.data, 'inbox') ?? boxes.data[0];
|
|
49
65
|
if (inbox)
|
|
50
66
|
setMailboxId(inbox.id);
|
|
51
|
-
}, [boxes.data, mailboxId]);
|
|
67
|
+
}, [boxes.data, mailboxId, setMailboxId]);
|
|
52
68
|
const threads = useThreads({ mailboxId: mailboxId ?? undefined, text: query, position, limit: PAGE, calculateTotal: true }, client);
|
|
53
69
|
const thread = useThread(threadId, client);
|
|
54
70
|
const refresh = useCallback(() => {
|
|
@@ -56,14 +72,24 @@ export function Mail({ client, now, className }) {
|
|
|
56
72
|
threads.reload();
|
|
57
73
|
}, [boxes.reload, threads.reload]);
|
|
58
74
|
usePush(refresh, client);
|
|
75
|
+
/* Errors reach the application, but only when they change: these hooks hold
|
|
76
|
+
their last error while a retry is in flight, so forwarding on every
|
|
77
|
+
render would call `onError` sixty times for one dead token. */
|
|
78
|
+
const failure = boxes.error ?? threads.error ?? thread.error ?? identities.error;
|
|
79
|
+
const reported = useRef(undefined);
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (!failure || reported.current === failure)
|
|
82
|
+
return;
|
|
83
|
+
reported.current = failure;
|
|
84
|
+
onError?.(failure);
|
|
85
|
+
}, [failure, onError]);
|
|
59
86
|
const go = useCallback((mailbox) => {
|
|
60
|
-
|
|
61
|
-
setThreadId(null);
|
|
87
|
+
navigate({ mailboxId: mailbox.id, threadId: null });
|
|
62
88
|
setPosition(0);
|
|
63
89
|
setQuery('');
|
|
64
90
|
setText('');
|
|
65
|
-
}, []);
|
|
66
|
-
const openThread = useCallback((summary) => setThreadId(summary.id), []);
|
|
91
|
+
}, [navigate]);
|
|
92
|
+
const openThread = useCallback((summary) => setThreadId(summary.id), [setThreadId]);
|
|
67
93
|
/* Cmd-K, and Escape out of a search. Bound on the window rather than on a
|
|
68
94
|
container, because the palette has to open from wherever focus is --
|
|
69
95
|
including from inside the message being read. */
|
|
@@ -145,5 +171,5 @@ export function Mail({ client, now, className }) {
|
|
|
145
171
|
return;
|
|
146
172
|
setText('');
|
|
147
173
|
setQuery('');
|
|
148
|
-
} }) }), _jsx(Button, { kind: "ghost", onClick: () => setPaletteOpen(true), "aria-label": "Open the palette", children: _jsx(Icon, { name: "bolt" }) })] }), _jsxs(SplitPane, { className: "mail-panes", label: "Mailbox list width", defaultSize: 22, min: 14, max: 40, storageKey: "mail-sidebar", children: [_jsx(MailboxTree, { mailboxes: boxes.data ?? [], selectedId: mailboxId, onSelect: go, loading: boxes.loading }), _jsxs(SplitPane, { label: "Conversation list width", defaultSize: 38, min: 22, max: 60, storageKey: "mail-list", children: [_jsx(ThreadList, { page: threads.data, selectedId: threadId, onSelect: openThread, onPage: setPosition, limit: PAGE, loading: threads.loading, now: now, empty: query ? _jsxs("p", { className: "threads-quiet", children: ["Nothing matches \u201C", query, "\u201D."] }) : undefined }), _jsx(ThreadView, { thread: thread.data, loading: thread.loading, onReply: startReply, onForward: startForward })] })] }), _jsx(Command, { open: paletteOpen, onOpenChange: setPaletteOpen, groups: paletteGroups }), draft && (_jsx(Modal, { title: "New message", width: "46rem", onClose: sending ? undefined : () => setDraft(null), closeDisabled: sending, children: _jsx(Composer, { draft: draft, onChange: setDraft, onSend: doSend, onCancel: () => setDraft(null), identities: identities.data ?? [], sending: sending, error: sendError }) }))] }));
|
|
174
|
+
} }) }), _jsx(Button, { kind: "ghost", onClick: () => setPaletteOpen(true), "aria-label": "Open the palette", children: _jsx(Icon, { name: "bolt" }) }), account] }), _jsxs(SplitPane, { className: "mail-panes", label: "Mailbox list width", defaultSize: 22, min: 14, max: 40, storageKey: "mail-sidebar", children: [_jsx(MailboxTree, { mailboxes: boxes.data ?? [], selectedId: mailboxId, onSelect: go, loading: boxes.loading }), _jsxs(SplitPane, { label: "Conversation list width", defaultSize: 38, min: 22, max: 60, storageKey: "mail-list", children: [_jsx(ThreadList, { page: threads.data, selectedId: threadId, onSelect: openThread, onPage: setPosition, limit: PAGE, loading: threads.loading, now: now, empty: query ? _jsxs("p", { className: "threads-quiet", children: ["Nothing matches \u201C", query, "\u201D."] }) : undefined }), _jsx(ThreadView, { thread: thread.data, loading: thread.loading, onReply: startReply, onForward: startForward })] })] }), _jsx(Command, { open: paletteOpen, onOpenChange: setPaletteOpen, groups: paletteGroups }), draft && (_jsx(Modal, { title: "New message", width: "46rem", onClose: sending ? undefined : () => setDraft(null), closeDisabled: sending, children: _jsx(Composer, { draft: draft, onChange: setDraft, onSend: doSend, onCancel: () => setDraft(null), identities: identities.data ?? [], sending: sending, error: sendError }) }))] }));
|
|
149
175
|
}
|
|
@@ -21,4 +21,4 @@ export type { ThreadViewProps } from './ThreadView.tsx';
|
|
|
21
21
|
export { Composer, parseAddresses } from './Composer.tsx';
|
|
22
22
|
export type { ComposerProps } from './Composer.tsx';
|
|
23
23
|
export { Mail } from './Mail.tsx';
|
|
24
|
-
export type { MailProps } from './Mail.tsx';
|
|
24
|
+
export type { MailLocation, MailProps } from './Mail.tsx';
|
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.
|