@push.rocks/smartjmap 1.0.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/.smartconfig.json +56 -0
- package/dist_ts/00_commitinfo_data.d.ts +8 -0
- package/dist_ts/00_commitinfo_data.js +9 -0
- package/dist_ts/classes.jmapclient.d.ts +236 -0
- package/dist_ts/classes.jmapclient.js +650 -0
- package/dist_ts/classes.jmapserver.d.ts +110 -0
- package/dist_ts/classes.jmapserver.js +873 -0
- package/dist_ts/index.d.ts +2 -0
- package/dist_ts/index.js +3 -0
- package/dist_ts/smartjmap.plugins.d.ts +4 -0
- package/dist_ts/smartjmap.plugins.js +6 -0
- package/license +19 -0
- package/npmextra.json +39 -0
- package/package.json +68 -0
- package/readme.hints.md +34 -0
- package/readme.md +287 -0
- package/ts/00_commitinfo_data.ts +8 -0
- package/ts/classes.jmapclient.ts +872 -0
- package/ts/classes.jmapserver.ts +1019 -0
- package/ts/index.ts +2 -0
- package/ts/smartjmap.plugins.ts +10 -0
package/dist_ts/index.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export * from './classes.jmapclient.js';
|
|
2
|
+
export * from './classes.jmapserver.js';
|
|
3
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxjQUFjLHlCQUF5QixDQUFDO0FBQ3hDLGNBQWMseUJBQXlCLENBQUMifQ==
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// node native
|
|
2
|
+
import * as events from 'node:events';
|
|
3
|
+
import * as http from 'node:http';
|
|
4
|
+
import * as net from 'node:net';
|
|
5
|
+
export { events, http, net, };
|
|
6
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic21hcnRqbWFwLnBsdWdpbnMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9zbWFydGptYXAucGx1Z2lucy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxjQUFjO0FBQ2QsT0FBTyxLQUFLLE1BQU0sTUFBTSxhQUFhLENBQUM7QUFDdEMsT0FBTyxLQUFLLElBQUksTUFBTSxXQUFXLENBQUM7QUFDbEMsT0FBTyxLQUFLLEdBQUcsTUFBTSxVQUFVLENBQUM7QUFFaEMsT0FBTyxFQUNMLE1BQU0sRUFDTixJQUFJLEVBQ0osR0FBRyxHQUNKLENBQUEifQ==
|
package/license
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2026 Task Venture Capital GmbH <hello@task.vc>
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
package/npmextra.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"@git.zone/cli": {
|
|
3
|
+
"projectType": "npm",
|
|
4
|
+
"module": {
|
|
5
|
+
"githost": "code.foss.global",
|
|
6
|
+
"gitscope": "push.rocks",
|
|
7
|
+
"gitrepo": "smartjmap",
|
|
8
|
+
"shortDescription": "JMAP client for mail (RFC 8620/8621)",
|
|
9
|
+
"description": "A TypeScript JMAP client for reading, sending, and watching mail via the JSON Meta Application Protocol (RFC 8620/8621), with a bundled test server for offline testing.",
|
|
10
|
+
"npmPackagename": "@push.rocks/smartjmap",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"projectDomain": "push.rocks",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"jmap",
|
|
15
|
+
"email",
|
|
16
|
+
"mail",
|
|
17
|
+
"rfc8620",
|
|
18
|
+
"rfc8621",
|
|
19
|
+
"fastmail",
|
|
20
|
+
"stalwart",
|
|
21
|
+
"json",
|
|
22
|
+
"http",
|
|
23
|
+
"client",
|
|
24
|
+
"typescript",
|
|
25
|
+
"event-driven",
|
|
26
|
+
"push",
|
|
27
|
+
"eventsource",
|
|
28
|
+
"message processing"
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"@ship.zone/szci": {
|
|
33
|
+
"npmGlobalTools": [],
|
|
34
|
+
"npmRegistryUrl": "registry.npmjs.org"
|
|
35
|
+
},
|
|
36
|
+
"@git.zone/tsdoc": {
|
|
37
|
+
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
|
|
38
|
+
}
|
|
39
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@push.rocks/smartjmap",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "A TypeScript JMAP client for reading, sending, and watching mail via the JSON Meta Application Protocol (RFC 8620/8621), with a bundled test server for offline testing.",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./dist_ts/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "dist_ts/index.js",
|
|
10
|
+
"typings": "dist_ts/index.d.ts",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"author": "Task Venture Capital GmbH <hello@task.vc>",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@git.zone/tsbuild": "^4.4.0",
|
|
16
|
+
"@git.zone/tsrun": "^2.0.3",
|
|
17
|
+
"@git.zone/tstest": "^3.6.3",
|
|
18
|
+
"@types/node": "^25.6.0"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://code.foss.global/push.rocks/smartjmap.git"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://code.foss.global/push.rocks/smartjmap/issues"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://code.foss.global/push.rocks/smartjmap#readme",
|
|
29
|
+
"browserslist": [
|
|
30
|
+
"last 1 chrome versions"
|
|
31
|
+
],
|
|
32
|
+
"files": [
|
|
33
|
+
"ts/**/*",
|
|
34
|
+
"ts_web/**/*",
|
|
35
|
+
"dist/**/*",
|
|
36
|
+
"dist_*/**/*",
|
|
37
|
+
"dist_ts/**/*",
|
|
38
|
+
"dist_ts_web/**/*",
|
|
39
|
+
"assets/**/*",
|
|
40
|
+
"cli.js",
|
|
41
|
+
".smartconfig.json",
|
|
42
|
+
"license",
|
|
43
|
+
"npmextra.json",
|
|
44
|
+
"readme.md"
|
|
45
|
+
],
|
|
46
|
+
"keywords": [
|
|
47
|
+
"jmap",
|
|
48
|
+
"email",
|
|
49
|
+
"mail",
|
|
50
|
+
"rfc8620",
|
|
51
|
+
"rfc8621",
|
|
52
|
+
"fastmail",
|
|
53
|
+
"stalwart",
|
|
54
|
+
"json",
|
|
55
|
+
"http",
|
|
56
|
+
"client",
|
|
57
|
+
"typescript",
|
|
58
|
+
"event-driven",
|
|
59
|
+
"push",
|
|
60
|
+
"eventsource",
|
|
61
|
+
"message processing"
|
|
62
|
+
],
|
|
63
|
+
"scripts": {
|
|
64
|
+
"test": "tstest test/",
|
|
65
|
+
"build": "tsbuild --web",
|
|
66
|
+
"buildDocs": "tsdoc"
|
|
67
|
+
}
|
|
68
|
+
}
|
package/readme.hints.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Project Readme Hints
|
|
2
|
+
|
|
3
|
+
Design notes for @push.rocks/smartjmap.
|
|
4
|
+
|
|
5
|
+
## HTTP layer
|
|
6
|
+
|
|
7
|
+
- Zero runtime dependencies: global `fetch` (Node.js 18+ floor; developed against @types/node 25). smartrequest would add no value for plain JSON-over-HTTP.
|
|
8
|
+
- SSE is parsed by hand over a fetch body stream (~50 lines in `runEventStream`). The global `EventSource` was rejected deliberately: it is only default-enabled from Node 22.3 and, more importantly, the WHATWG EventSource API cannot send the `Authorization` header JMAP requires.
|
|
9
|
+
|
|
10
|
+
## Watching semantics
|
|
11
|
+
|
|
12
|
+
- `connect()`: session GET → capability/account validation → mailbox resolution (role match on the lowercased mailbox name, e.g. 'INBOX' → role 'inbox', falling back to case-insensitive name match) → Email state snapshot → 'connected' → initial sweep (Email/query + Email/get, emitted oldest-first) → event stream.
|
|
13
|
+
- The Email state is snapshotted *before* the initial sweep and `fetchDeltas()` runs once right after the SSE stream is established, so mail arriving between connect and stream establishment is not lost; `seenEmailIds` deduplicates the overlap.
|
|
14
|
+
- Event stream errors/end → silent fallback to `Email/changes` polling on `pollIntervalMs`. `cannotCalculateChanges` → full re-query resync.
|
|
15
|
+
- `connect()` emits 'error' instead of throwing (mirrors smartimap). Unlike smartimap, the same instance may call `connect()` again — JMAP is stateless HTTP.
|
|
16
|
+
- `disconnect()` aborts the SSE fetch and clears the poll timer; the connect-failure path also tears both down, so no handles leak either way.
|
|
17
|
+
|
|
18
|
+
## RFC subset covered (1.0.0)
|
|
19
|
+
|
|
20
|
+
- RFC 8620: session resource, method calls/responses, `error` method responses, RFC 7807 problem details, `#creationId` back-references (client-side use; server resolves them in EmailSubmission/set), download URL template expansion, event source (SSE StateChange).
|
|
21
|
+
- RFC 8621: Mailbox/get(+query on server), Email/query, Email/get with `fetchAllBodyValues`, Email/changes, Email/set (create/update with keyword patches/destroy), Identity/get, EmailSubmission/set.
|
|
22
|
+
|
|
23
|
+
## Known gaps
|
|
24
|
+
|
|
25
|
+
- No upload endpoint / Blob upload (`uploadUrl` is advertised by the test server but not implemented), so `sendEmail` supports text/html bodies but not attachments.
|
|
26
|
+
- No Thread/*, SearchSnippet/*, VacationResponse/*, Mailbox/set, Mailbox/changes, EmailSubmission onSuccessUpdateEmail, or result references (`resultOf`) — the client issues explicit-id calls instead.
|
|
27
|
+
- `Email/get` on the test server ignores the `properties` projection and always returns the full superset; `Email/query` supports only `inMailbox`/`subject`/`text` filters and `receivedAt` sorting.
|
|
28
|
+
- The client requests event-source pushes with `types=*` and reacts to Email state changes only; Mailbox counters are not tracked incrementally.
|
|
29
|
+
- Initial-sweep race: an email created between the Email/query and Email/get of the sweep is picked up by the following delta fetch, not the sweep itself (dedupe prevents double emission).
|
|
30
|
+
|
|
31
|
+
## By-design unbounded state (accepted)
|
|
32
|
+
|
|
33
|
+
- Client: `seenEmailIds` retains every emitted email id for the instance lifetime — required for dedupe across reconnects and sweep/delta overlap; on a very long-lived, high-volume connection this grows without a pruning hook. Recreate the client to reset it.
|
|
34
|
+
- Test server (in-memory helper, not production code): the per-account change log grows one entry per mutation (needed by `Email/changes`), destroyed emails leave their blobs in the blob store, and request bodies are buffered without enforcing the advertised `maxSizeRequest`.
|
package/readme.md
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# @push.rocks/smartjmap
|
|
2
|
+
|
|
3
|
+
A TypeScript JMAP client for reading, sending, and watching mail via the JSON Meta Application Protocol (RFC 8620/8621), with a bundled test server for offline testing.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
To install `@push.rocks/smartjmap`, use pnpm (or npm):
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pnpm install @push.rocks/smartjmap
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The package has zero runtime dependencies — it uses the global `fetch` available in Node.js 18+.
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
`@push.rocks/smartjmap` connects to any JMAP server (Fastmail, Stalwart, Cyrus, ...), watches a mailbox for new mail in an event-driven way, and exposes the common mail operations: querying, reading, flagging, sending, and blob downloads.
|
|
18
|
+
|
|
19
|
+
### Importing the Required Modules
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { JmapClient, type IJmapClientConfig } from '@push.rocks/smartjmap';
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Configuration Object
|
|
26
|
+
|
|
27
|
+
Point the client at the JMAP session resource — most servers expose it at the autodiscovery path `https://host/.well-known/jmap`. All URLs inside the session (API endpoint, download URL, event source) are resolved relative to this URL automatically.
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
const jmapConfig: IJmapClientConfig = {
|
|
31
|
+
sessionUrl: 'https://jmap.example.com/.well-known/jmap',
|
|
32
|
+
auth: {
|
|
33
|
+
accessToken: 'my-api-token',
|
|
34
|
+
},
|
|
35
|
+
mailbox: 'INBOX', // default: resolved via Mailbox role 'inbox', falling back to a name match
|
|
36
|
+
pollIntervalMs: 30000, // fallback polling cadence when the event stream is unavailable
|
|
37
|
+
};
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Authentication: Bearer and Basic
|
|
41
|
+
|
|
42
|
+
The `auth` option accepts exactly one of two shapes:
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import { JmapClient } from '@push.rocks/smartjmap';
|
|
46
|
+
|
|
47
|
+
// Bearer token (OAuth2 access token or server API token)
|
|
48
|
+
const bearerClient = new JmapClient({
|
|
49
|
+
sessionUrl: 'https://jmap.example.com/.well-known/jmap',
|
|
50
|
+
auth: {
|
|
51
|
+
accessToken: 'my-api-token',
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Basic auth (username + password)
|
|
56
|
+
const basicClient = new JmapClient({
|
|
57
|
+
sessionUrl: 'https://jmap.example.com/.well-known/jmap',
|
|
58
|
+
auth: {
|
|
59
|
+
user: 'user@example.com',
|
|
60
|
+
pass: 'password123',
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Passing both shapes at once (or neither) throws at construction time.
|
|
66
|
+
|
|
67
|
+
### Connecting and Handling Events
|
|
68
|
+
|
|
69
|
+
`connect()` fetches and validates the JMAP session (the `urn:ietf:params:jmap:core` and `urn:ietf:params:jmap:mail` capabilities are required), resolves the target mailbox, and emits `connected`. It then emits a `message` event for every email currently in the mailbox and keeps watching for new mail: it prefers the JMAP event source (Server-Sent Events, RFC 8620 §7.3) and falls back to polling `Email/changes` on `pollIntervalMs` when the event stream is unavailable.
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import { JmapClient, type ISmartJmapMessage } from '@push.rocks/smartjmap';
|
|
73
|
+
|
|
74
|
+
const jmapClient = new JmapClient({
|
|
75
|
+
sessionUrl: 'https://jmap.example.com/.well-known/jmap',
|
|
76
|
+
auth: { accessToken: 'my-api-token' },
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
jmapClient.on('connected', () => {
|
|
80
|
+
console.log('Connected to the JMAP server');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
jmapClient.on('message', (message: ISmartJmapMessage) => {
|
|
84
|
+
console.log('From:', message.from[0]?.email);
|
|
85
|
+
console.log('Subject:', message.subject);
|
|
86
|
+
console.log('Text:', message.textBody);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
jmapClient.on('error', (error: Error) => {
|
|
90
|
+
console.error('JMAP error:', error);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
jmapClient.on('disconnected', () => {
|
|
94
|
+
console.log('Disconnected');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
await jmapClient.connect();
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### The Message Shape
|
|
101
|
+
|
|
102
|
+
Every `message` event (and the query/get helpers) delivers an `ISmartJmapMessage` with resolved bodies:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
jmapClient.on('message', (message: ISmartJmapMessage) => {
|
|
106
|
+
message.id; // JMAP Email id
|
|
107
|
+
message.blobId; // blob id of the raw RFC 5322 message
|
|
108
|
+
message.threadId; // JMAP Thread id
|
|
109
|
+
message.mailboxIds; // { [mailboxId]: true }
|
|
110
|
+
message.keywords; // { '$seen': true, ... }
|
|
111
|
+
message.from; // IJmapEmailAddress[]
|
|
112
|
+
message.to; // IJmapEmailAddress[] (cc, bcc, replyTo likewise)
|
|
113
|
+
message.subject; // string | undefined
|
|
114
|
+
message.receivedAt; // ISO date string
|
|
115
|
+
message.textBody; // plain-text body, resolved from bodyValues
|
|
116
|
+
message.htmlBody; // HTML body, undefined when the email has no text/html part
|
|
117
|
+
message.attachments; // IJmapAttachment[]: { blobId, name, type, size }
|
|
118
|
+
message.raw; // the full raw JMAP Email object
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Querying and Reading Mail
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
// All mailboxes of the account
|
|
126
|
+
const mailboxes = await jmapClient.getMailboxes();
|
|
127
|
+
|
|
128
|
+
// Emails in the watched mailbox, newest first (default filter)
|
|
129
|
+
const messages = await jmapClient.queryEmails();
|
|
130
|
+
|
|
131
|
+
// Custom JMAP filter (RFC 8621 §4.4.1) and limit
|
|
132
|
+
const invoices = await jmapClient.queryEmails({ subject: 'Invoice' }, 10);
|
|
133
|
+
|
|
134
|
+
// Single email with all body values fetched (fetchAllBodyValues)
|
|
135
|
+
const message = await jmapClient.getEmail(messages[0].id);
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Flags and Keywords
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
// Convenience: set the $seen keyword
|
|
142
|
+
await jmapClient.markSeen(message.id);
|
|
143
|
+
|
|
144
|
+
// Replace the full keywords object
|
|
145
|
+
await jmapClient.setKeywords(message.id, { $seen: true, $flagged: true });
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Sending Mail
|
|
149
|
+
|
|
150
|
+
`sendEmail` looks up the account's identities via `Identity/get`, creates the email via `Email/set` (stored in the mailbox with role `sent`, falling back to `drafts`, then the watched mailbox), and submits it via `EmailSubmission/set`:
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
const result = await jmapClient.sendEmail({
|
|
154
|
+
to: [{ email: 'friend@example.com' }],
|
|
155
|
+
subject: 'Hello from JMAP',
|
|
156
|
+
textBody: 'Plain-text content',
|
|
157
|
+
htmlBody: '<p>HTML content</p>', // optional
|
|
158
|
+
});
|
|
159
|
+
console.log(result.emailId, result.submissionId);
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
When `from` is omitted, the first identity of the account is used.
|
|
163
|
+
|
|
164
|
+
### Downloading Blobs
|
|
165
|
+
|
|
166
|
+
Raw messages and attachments are blobs; download them via the session's download URL template:
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
// the raw RFC 5322 message
|
|
170
|
+
const rawBytes = await jmapClient.downloadBlob(message.blobId, 'message/rfc822', 'message.eml');
|
|
171
|
+
|
|
172
|
+
// an attachment
|
|
173
|
+
for (const attachment of message.attachments) {
|
|
174
|
+
const bytes = await jmapClient.downloadBlob(attachment.blobId, attachment.type, attachment.name);
|
|
175
|
+
console.log(attachment.name, bytes.length);
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Raw JMAP Method Calls
|
|
180
|
+
|
|
181
|
+
For anything not covered by the helpers, `request` sends raw JMAP method calls (RFC 8620 §3.2) and returns the method responses:
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
const methodResponses = await jmapClient.request([
|
|
185
|
+
['Mailbox/query', { accountId: 'account-id', filter: { role: 'archive' } }, 'q0'],
|
|
186
|
+
]);
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Disconnecting
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
await jmapClient.disconnect();
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
`disconnect()` tears down the event stream and any polling timer — no handles are left open — and emits `disconnected`.
|
|
196
|
+
|
|
197
|
+
### The Bundled Test Server
|
|
198
|
+
|
|
199
|
+
`JmapServer` is a minimal in-memory JMAP server for offline tests. It serves the session resource at `/.well-known/jmap` (and `/jmap/session`), the API at `/jmap/api`, blob downloads, and StateChange pushes via SSE at `/jmap/eventsource`. It validates Bearer and Basic auth and answers unauthenticated requests with `401` + `WWW-Authenticate`.
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
import { JmapClient, JmapServer } from '@push.rocks/smartjmap';
|
|
203
|
+
|
|
204
|
+
const jmapServer = new JmapServer();
|
|
205
|
+
jmapServer.addUser('testuser', 'testpass');
|
|
206
|
+
jmapServer.addBearerToken('testuser', 'test-token');
|
|
207
|
+
jmapServer.createMailbox('testuser', 'INBOX', 'inbox');
|
|
208
|
+
jmapServer.addEmail('testuser', 'INBOX', {
|
|
209
|
+
from: { email: 'alice@example.com' },
|
|
210
|
+
to: { email: 'testuser@example.com' },
|
|
211
|
+
subject: 'Welcome',
|
|
212
|
+
textBody: 'Hello from the test server!',
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const port = await jmapServer.start(0); // resolves with the bound port
|
|
216
|
+
|
|
217
|
+
const client = new JmapClient({
|
|
218
|
+
sessionUrl: `http://127.0.0.1:${port}/.well-known/jmap`,
|
|
219
|
+
auth: { accessToken: 'test-token' },
|
|
220
|
+
});
|
|
221
|
+
client.on('message', (message) => console.log(message.subject));
|
|
222
|
+
await client.connect();
|
|
223
|
+
|
|
224
|
+
// Emails added while a client is connected are pushed via SSE:
|
|
225
|
+
jmapServer.addEmail('testuser', 'INBOX', {
|
|
226
|
+
from: { email: 'bob@example.com' },
|
|
227
|
+
to: { email: 'testuser@example.com' },
|
|
228
|
+
subject: 'Live push',
|
|
229
|
+
textBody: 'Delivered through the event stream.',
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
await client.disconnect();
|
|
233
|
+
await jmapServer.stop(); // closes the server and destroys open connections
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
The dispatched methods are `Core/echo`, `Mailbox/get`, `Mailbox/query`, `Email/get`, `Email/query`, `Email/changes`, `Email/set`, `Identity/get`, and `EmailSubmission/set` — enough for every public `JmapClient` method to run offline.
|
|
237
|
+
|
|
238
|
+
### Error Handling
|
|
239
|
+
|
|
240
|
+
`connect()` never throws; failures (unreachable host, wrong credentials, missing capabilities) are emitted as `error` events, and the instance holds no open sockets or timers afterwards. HTTP problem details (RFC 7807) and JMAP method-level errors are surfaced as `JmapError` with `httpStatus`, `problemType`, `problemDetail`, `jmapErrorType`, and `jmapErrorDescription` fields.
|
|
241
|
+
|
|
242
|
+
Because JMAP is stateless HTTP, the same client instance can reconnect — call `connect()` again after a failure. Already-emitted messages are deduplicated, so a reconnect does not re-emit mail the client has already seen. While connected, the client recovers on its own: if the event stream drops, it silently falls back to `Email/changes` polling, and a `cannotCalculateChanges` response triggers a full re-query.
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
import { JmapClient, JmapError } from '@push.rocks/smartjmap';
|
|
246
|
+
|
|
247
|
+
const client = new JmapClient({
|
|
248
|
+
sessionUrl: 'https://jmap.example.com/.well-known/jmap',
|
|
249
|
+
auth: { accessToken: 'my-api-token' },
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
client.on('error', (error: Error) => {
|
|
253
|
+
if (error instanceof JmapError && error.httpStatus === 401) {
|
|
254
|
+
console.error('Credentials rejected — refresh the token before reconnecting.');
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
console.error('JMAP error, retrying in 10s:', error.message);
|
|
258
|
+
setTimeout(() => {
|
|
259
|
+
client.connect().catch(console.error);
|
|
260
|
+
}, 10000);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
await client.connect();
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Issue Reporting and Security
|
|
267
|
+
|
|
268
|
+
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
|
269
|
+
|
|
270
|
+
## License and Legal Information
|
|
271
|
+
|
|
272
|
+
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
|
|
273
|
+
|
|
274
|
+
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
|
275
|
+
|
|
276
|
+
### Trademarks
|
|
277
|
+
|
|
278
|
+
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.
|
|
279
|
+
|
|
280
|
+
### Company Information
|
|
281
|
+
|
|
282
|
+
Task Venture Capital GmbH
|
|
283
|
+
Registered at District court Bremen HRB 35230 HB, Germany
|
|
284
|
+
|
|
285
|
+
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
|
|
286
|
+
|
|
287
|
+
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* autocreated commitinfo by @push.rocks/commitinfo
|
|
3
|
+
*/
|
|
4
|
+
export const commitinfo = {
|
|
5
|
+
name: '@push.rocks/smartjmap',
|
|
6
|
+
version: '1.0.0',
|
|
7
|
+
description: 'A TypeScript JMAP client for reading, sending, and watching mail via the JSON Meta Application Protocol (RFC 8620/8621), with a bundled test server for offline testing.'
|
|
8
|
+
}
|