imapkit 5.2.0 → 5.3.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/README.md +37 -21
- package/bin/help.txt +25 -1
- package/bin/imapkit.js +35 -0
- package/dist/browser/imapkit.js +99 -35
- package/dist/cjs/control.d.ts +16 -5
- package/dist/cjs/control.js +35 -4
- package/dist/cjs/oauth/http.d.ts +28 -0
- package/dist/cjs/oauth/http.js +216 -0
- package/dist/cjs/oauth/jwt.d.ts +33 -0
- package/dist/cjs/oauth/jwt.js +58 -0
- package/dist/cjs/oauth/pages.d.ts +47 -0
- package/dist/cjs/oauth/pages.js +310 -0
- package/dist/cjs/oauth/policy.d.ts +48 -0
- package/dist/cjs/oauth/policy.js +66 -0
- package/dist/cjs/oauth/provider.d.ts +455 -0
- package/dist/cjs/oauth/provider.js +1422 -0
- package/dist/cjs/plugins/oauthbearer.js +10 -15
- package/dist/cjs/plugins/xoauth2.js +4 -3
- package/dist/cjs/rest.d.ts +18 -1
- package/dist/cjs/rest.js +49 -31
- package/dist/cjs/server.d.ts +25 -1
- package/dist/cjs/server.js +73 -11
- package/dist/cjs/smtp-listener.d.ts +3 -2
- package/dist/cjs/smtp-listener.js +15 -3
- package/dist/cjs/types.d.ts +10 -0
- package/dist/esm/control.d.ts +16 -5
- package/dist/esm/control.js +35 -5
- package/dist/esm/oauth/http.d.ts +28 -0
- package/dist/esm/oauth/http.js +211 -0
- package/dist/esm/oauth/jwt.d.ts +33 -0
- package/dist/esm/oauth/jwt.js +50 -0
- package/dist/esm/oauth/pages.d.ts +47 -0
- package/dist/esm/oauth/pages.js +305 -0
- package/dist/esm/oauth/policy.d.ts +48 -0
- package/dist/esm/oauth/policy.js +59 -0
- package/dist/esm/oauth/provider.d.ts +455 -0
- package/dist/esm/oauth/provider.js +1415 -0
- package/dist/esm/plugins/oauthbearer.js +10 -15
- package/dist/esm/plugins/xoauth2.js +4 -3
- package/dist/esm/rest.d.ts +18 -1
- package/dist/esm/rest.js +48 -32
- package/dist/esm/server.d.ts +25 -1
- package/dist/esm/server.js +74 -12
- package/dist/esm/smtp-listener.d.ts +3 -2
- package/dist/esm/smtp-listener.js +15 -3
- package/dist/esm/types.d.ts +10 -0
- package/dist/plugin-help.json +2 -2
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -142,6 +142,21 @@ ImapKit follows these of the strategies that RFC 2180 (IMAP4 Multi-Accessed Mail
|
|
|
142
142
|
|
|
143
143
|
By default the only account is user name `"testuser"` with password `"testpass"` (and access token `"testtoken"` for XOAUTH2 and OAUTHBEARER). The `users` option replaces the default account list, for example `{ "testuser": { "password": "testpass" }, "otheruser": { "password": "secret" } }`, and `server.control.addUser()` adds users at runtime (see [Control API](#control-api)). All users share the same mailbox tree.
|
|
144
144
|
|
|
145
|
+
## OAuth 2.0 and OpenID Connect
|
|
146
|
+
|
|
147
|
+
The `oauth` option (`--oauth-port`) starts a test identity provider, so a mail client can run its whole OAuth sign-in against ImapKit: the authorization code flow with PKCE, a consent page that needs no password, and tokens that log in to IMAP (XOAUTH2, OAUTHBEARER) and SMTP (AUTH XOAUTH2), expire on the server clock, refresh and can be revoked.
|
|
148
|
+
|
|
149
|
+
```javascript
|
|
150
|
+
const server = imapkit({
|
|
151
|
+
plugins: ['SASL-IR', 'XOAUTH2', 'OAUTHBEARER'],
|
|
152
|
+
users: { 'alice@example.com': { password: 'pw', displayName: 'Alice' }, 'denied@example.com': { password: 'pw', oauth: { consent: 'deny' } } },
|
|
153
|
+
oauth: { port: 8085 }
|
|
154
|
+
});
|
|
155
|
+
await server.start(1143); // discovery: http://127.0.0.1:8085/.well-known/openid-configuration
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
A consent policy decides every sign-in, from the first that has one: a queued decision (`control.queueOAuthDecision()`), the user that `login_hint` names, the client, the server default. `prompt` shows the consent page (pick an account, Allow or Deny), `approve` and `deny` redirect right away with a plain HTTP 302, so a test can follow the redirect without a browser, and an error object answers with that error. The clients `imapkit`, `imapkit-approve` and `imapkit-deny` are registered by default, with loopback redirect URIs on any port. The ID token's `email` claim is the user name, so use address-style user names for OAuth tests. The provider is strict like the IMAP side: PKCE S256 for public clients, single-use codes, exact redirect URIs, no unknown scopes. Token faults, forced expiry and revocation are in the control API and the REST API. See [OAuth 2.0 and OpenID Connect](https://imapkit.com/docs/guides/oauth).
|
|
159
|
+
|
|
145
160
|
## Status
|
|
146
161
|
|
|
147
162
|
### IMAP4rev1
|
|
@@ -480,7 +495,7 @@ The REST API controls the whole server, including the mail of every user, so it
|
|
|
480
495
|
|
|
481
496
|
Requests and responses are JSON, and every POST needs `Content-Type: application/json`, also without a body (HTTP 415 otherwise). Errors are `{ "error": { "code", "message" } }` with HTTP status 404 for `NONEXISTENT`, 409 for `ALREADYEXISTS` and the response codes of a failed mailbox operation (`CANNOT`, `HASCHILDREN` ...), 400 for `INVALID`, 401 for a missing or wrong token, 404 `NOTFOUND` and 405 `METHOD` for an unknown route or method, 413 `TOOBIG` for a body over 64 MiB (and `addMessage` checks refused by APPENDLIMIT), 500 for a server error. A mailbox is its storage name (modified UTF-7) in the URL, URL encoded with a `/` in the name as `%2F` (`/v1/mailboxes/Work%2FProjects`). Message sources are base64 in responses (`"encoding": "base64"`), a request sends `raw` as text or as base64 with `"encoding": "base64"`. `GET /v1/openapi.json` describes every endpoint.
|
|
482
497
|
|
|
483
|
-
`GET /v1/events` streams the [events](#events) as [Server-Sent Events](https://html.spec.whatwg.org/multipage/server-sent-events.html) (`event: <type>` with JSON `data`), so a test in another language can wait for "the client selected INBOX" or "a script rule fired" without polling. `?types=session,command` chooses the types (`session`, `command`, `mailbox`, `append`, `expunge`, `flags`, `acl`, `script`, `reset`, and `trace`, which is only sent when asked for, with base64 `data`), mailboxes are paths, messages UIDs and sessions numbers in the data:
|
|
498
|
+
`GET /v1/events` streams the [events](#events) as [Server-Sent Events](https://html.spec.whatwg.org/multipage/server-sent-events.html) (`event: <type>` with JSON `data`), so a test in another language can wait for "the client selected INBOX" or "a script rule fired" without polling. `?types=session,command` chooses the types (`session`, `command`, `mailbox`, `append`, `expunge`, `flags`, `acl`, `script`, `reset`, `oauth`, and `trace`, which is only sent when asked for, with base64 `data`), mailboxes are paths, messages UIDs and sessions numbers in the data:
|
|
484
499
|
|
|
485
500
|
```bash
|
|
486
501
|
curl -N 'http://127.0.0.1:8143/v1/events?types=session'
|
|
@@ -488,26 +503,27 @@ curl -N 'http://127.0.0.1:8143/v1/events?types=session'
|
|
|
488
503
|
# data: {"type":"select","session":{"session":1,"user":"testuser","state":"Selected","mailbox":"INBOX",...}}
|
|
489
504
|
```
|
|
490
505
|
|
|
491
|
-
| Endpoint
|
|
492
|
-
|
|
|
493
|
-
| `GET /v1/snapshot`
|
|
494
|
-
| `POST /v1/reset`
|
|
495
|
-
| `POST /v1/shutdown` (`{ graceful }`)
|
|
496
|
-
| `GET /v1/sessions`, `DELETE /v1/sessions/{n}` (`{ text, reset }`)
|
|
497
|
-
| `POST /v1/sessions/{n}/inject` (`{ data, encoding }`)
|
|
498
|
-
| `GET`, `POST /v1/users`, `PUT`, `DELETE /v1/users/{name}`
|
|
499
|
-
| `GET`, `POST /v1/mailboxes`, `GET`, `DELETE /v1/mailboxes/{path}`
|
|
500
|
-
| `POST /v1/mailboxes/{path}/rename` (`{ newPath }`)
|
|
501
|
-
| `PUT`, `DELETE /v1/mailboxes/{path}/subscription`
|
|
502
|
-
| `POST /v1/mailboxes/{path}/uidvalidity`
|
|
503
|
-
| `GET /v1/mailboxes/{path}/messages` (`?uids=1,2&raw=true`), `POST` (`{ raw, flags }`)
|
|
504
|
-
| `GET`, `DELETE /v1/mailboxes/{path}/messages/{uid}`
|
|
505
|
-
| `POST /v1/mailboxes/{path}/messages/flags`, `.../expunge`, `.../copy`, `.../move`
|
|
506
|
-
| `GET /v1/mailboxes/{path}/acl`, `PUT`, `DELETE /v1/mailboxes/{path}/acl/{identifier}` (`{ rights }`)
|
|
507
|
-
| `GET`, `PUT /v1/quota`
|
|
508
|
-
| `GET`, `PUT /v1/metadata`, `GET`, `PUT /v1/mailboxes/{path}/metadata`
|
|
509
|
-
| `PUT /v1/mailboxes/{path}/special-use` (`{ specialUse }`)
|
|
510
|
-
| `GET`, `POST`, `DELETE /v1/script/rules`, `DELETE /v1/script/rules/{id}`
|
|
506
|
+
| Endpoint | Control API |
|
|
507
|
+
| ------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- |
|
|
508
|
+
| `GET /v1/snapshot` | `snapshot()` |
|
|
509
|
+
| `POST /v1/reset` | `reset()` |
|
|
510
|
+
| `POST /v1/shutdown` (`{ graceful }`) | `shutdown()` |
|
|
511
|
+
| `GET /v1/sessions`, `DELETE /v1/sessions/{n}` (`{ text, reset }`) | `sessions()`, `disconnect()` |
|
|
512
|
+
| `POST /v1/sessions/{n}/inject` (`{ data, encoding }`) | `inject()` |
|
|
513
|
+
| `GET`, `POST /v1/users`, `PUT`, `DELETE /v1/users/{name}` | `listUsers()`, `addUser()`, `updateUser()`, `deleteUser()` |
|
|
514
|
+
| `GET`, `POST /v1/mailboxes`, `GET`, `DELETE /v1/mailboxes/{path}` | `listMailboxes()`, `createMailbox()`, `getMailbox()`, `deleteMailbox()` |
|
|
515
|
+
| `POST /v1/mailboxes/{path}/rename` (`{ newPath }`) | `renameMailbox()` |
|
|
516
|
+
| `PUT`, `DELETE /v1/mailboxes/{path}/subscription` | `subscribe()`, `unsubscribe()` |
|
|
517
|
+
| `POST /v1/mailboxes/{path}/uidvalidity` | `resetUidValidity()` |
|
|
518
|
+
| `GET /v1/mailboxes/{path}/messages` (`?uids=1,2&raw=true`), `POST` (`{ raw, flags }`) | `listMessages()`, `addMessage()` |
|
|
519
|
+
| `GET`, `DELETE /v1/mailboxes/{path}/messages/{uid}` | `getMessage()`, `expungeMessages()` |
|
|
520
|
+
| `POST /v1/mailboxes/{path}/messages/flags`, `.../expunge`, `.../copy`, `.../move` | `setFlags()`, `expungeMessages()`, `copyMessages()`, `moveMessages()` |
|
|
521
|
+
| `GET /v1/mailboxes/{path}/acl`, `PUT`, `DELETE /v1/mailboxes/{path}/acl/{identifier}` (`{ rights }`) | `getAcl()`, `setAcl()`, `deleteAcl()` (ACL) |
|
|
522
|
+
| `GET`, `PUT /v1/quota` | `getQuota()`, `setQuota()` (QUOTA) |
|
|
523
|
+
| `GET`, `PUT /v1/metadata`, `GET`, `PUT /v1/mailboxes/{path}/metadata` | `getMetadata()`, `setMetadata()` (METADATA) |
|
|
524
|
+
| `PUT /v1/mailboxes/{path}/special-use` (`{ specialUse }`) | `setSpecialUse()` (SPECIAL-USE) |
|
|
525
|
+
| `GET`, `POST`, `DELETE /v1/script/rules`, `DELETE /v1/script/rules/{id}` | [script rules](#scripted-faults) in their JSON form, so runtime faults need no restart |
|
|
526
|
+
| `GET /v1/oauth`, `/v1/oauth/clients`, `/v1/oauth/consent`, `/v1/oauth/decisions`, `/v1/oauth/token-faults`, `/v1/oauth/tokens` | the [OAuth provider](#oauth-20-and-openid-connect), with the `oauth` option |
|
|
511
527
|
|
|
512
528
|
## Scripted faults
|
|
513
529
|
|
package/bin/help.txt
CHANGED
|
@@ -31,6 +31,15 @@ Usage: imapkit [OPTS]
|
|
|
31
31
|
--rest-host=<address> Address of the REST API, defaults to 127.0.0.1.
|
|
32
32
|
Any other than a loopback address needs a token
|
|
33
33
|
--rest-token=<token> Bearer token that REST requests must send
|
|
34
|
+
--oauth-port=<port> Starts the OAuth 2.0 / OpenID Connect provider:
|
|
35
|
+
tokens for IMAP (XOAUTH2, OAUTHBEARER) and SMTP
|
|
36
|
+
(AUTH XOAUTH2), no passwords, see "OAuth" below
|
|
37
|
+
--oauth-host=<address> Address of the OAuth provider, 127.0.0.1 default
|
|
38
|
+
--oauth-consent=<policy> What a sign-in does: prompt (consent page, the
|
|
39
|
+
default), approve or deny (redirect right away)
|
|
40
|
+
--oauth-secure=true Serves the OAuth provider over HTTPS with the
|
|
41
|
+
bundled localhost certificate
|
|
42
|
+
--oauth-issuer=<url> Public base URL of the OAuth provider (a tunnel)
|
|
34
43
|
--quirk=<name> Makes the server behave like a known real server
|
|
35
44
|
(james-fetchgroup, james-late-fetch,
|
|
36
45
|
yahoo-quoted-sections, m365-throttle, no-uidplus,
|
|
@@ -42,7 +51,9 @@ The options can also be set with environment variables: IMAPKIT_PORT,
|
|
|
42
51
|
IMAPKIT_SECURE, IMAPKIT_DEBUG, IMAPKIT_STORAGE, IMAPKIT_CONFIG,
|
|
43
52
|
IMAPKIT_SCRIPT, IMAPKIT_PLUGINS (comma separated), IMAPKIT_TLS_PORT,
|
|
44
53
|
IMAPKIT_SMTPPORT, IMAPKIT_REST_PORT, IMAPKIT_REST_HOST, IMAPKIT_REST_TOKEN,
|
|
45
|
-
|
|
54
|
+
IMAPKIT_OAUTH_PORT, IMAPKIT_OAUTH_HOST, IMAPKIT_OAUTH_CONSENT,
|
|
55
|
+
IMAPKIT_OAUTH_SECURE, IMAPKIT_OAUTH_ISSUER, IMAPKIT_QUIRKS and
|
|
56
|
+
IMAPKIT_SCRIPT_SEED.
|
|
46
57
|
|
|
47
58
|
NB! If port or smtpPort values are below 1024 you most probably need to
|
|
48
59
|
use sudo or run the command with administrator rights.
|
|
@@ -80,6 +91,19 @@ Configuration file takes the following structure
|
|
|
80
91
|
}
|
|
81
92
|
}
|
|
82
93
|
|
|
94
|
+
OAuth
|
|
95
|
+
-----
|
|
96
|
+
|
|
97
|
+
--oauth-port starts a test identity provider. Its discovery document is
|
|
98
|
+
at <issuer>/.well-known/openid-configuration. Three public clients are
|
|
99
|
+
registered (loopback redirect URIs on any port, PKCE S256 required):
|
|
100
|
+
"imapkit" follows the default consent policy, "imapkit-approve" always
|
|
101
|
+
approves and "imapkit-deny" always denies. The consent page needs no
|
|
102
|
+
password: pick an account and click Allow or Deny. The email claim is
|
|
103
|
+
the user name, so use address-style user names for OAuth tests. Load
|
|
104
|
+
SASL-IR with XOAUTH2 or OAUTHBEARER to log in to IMAP with the tokens.
|
|
105
|
+
More clients, scopes and policies go into "oauth" of the --config file.
|
|
106
|
+
|
|
83
107
|
Scripted faults
|
|
84
108
|
---------------
|
|
85
109
|
|
package/bin/imapkit.js
CHANGED
|
@@ -27,6 +27,11 @@ const { values: argv } = parseArgs({
|
|
|
27
27
|
'rest-port': { type: 'string' },
|
|
28
28
|
'rest-host': { type: 'string' },
|
|
29
29
|
'rest-token': { type: 'string' },
|
|
30
|
+
'oauth-port': { type: 'string' },
|
|
31
|
+
'oauth-host': { type: 'string' },
|
|
32
|
+
'oauth-consent': { type: 'string' },
|
|
33
|
+
'oauth-secure': { type: 'boolean' },
|
|
34
|
+
'oauth-issuer': { type: 'string' },
|
|
30
35
|
quirk: { type: 'string', multiple: true },
|
|
31
36
|
'script-seed': { type: 'string' }
|
|
32
37
|
}
|
|
@@ -53,6 +58,11 @@ const tlsPort = argv['tls-port'] || process.env.IMAPKIT_TLS_PORT;
|
|
|
53
58
|
const restPort = argv['rest-port'] || process.env.IMAPKIT_REST_PORT;
|
|
54
59
|
const restHost = argv['rest-host'] || process.env.IMAPKIT_REST_HOST;
|
|
55
60
|
const restToken = argv['rest-token'] || process.env.IMAPKIT_REST_TOKEN;
|
|
61
|
+
const oauthPort = argv['oauth-port'] || process.env.IMAPKIT_OAUTH_PORT;
|
|
62
|
+
const oauthHost = argv['oauth-host'] || process.env.IMAPKIT_OAUTH_HOST;
|
|
63
|
+
const oauthConsent = argv['oauth-consent'] || process.env.IMAPKIT_OAUTH_CONSENT;
|
|
64
|
+
const oauthSecure = isTrue(argv['oauth-secure'] || process.env.IMAPKIT_OAUTH_SECURE);
|
|
65
|
+
const oauthIssuer = argv['oauth-issuer'] || process.env.IMAPKIT_OAUTH_ISSUER;
|
|
56
66
|
const quirksList = listOption(argv.quirk || process.env.IMAPKIT_QUIRKS);
|
|
57
67
|
const scriptSeed = argv['script-seed'] || process.env.IMAPKIT_SCRIPT_SEED;
|
|
58
68
|
const pluginsList = listOption(argv.plugin || process.env.IMAPKIT_PLUGINS).map(plugin => plugin.toUpperCase());
|
|
@@ -123,6 +133,18 @@ if (argv.help) {
|
|
|
123
133
|
if (restPort) {
|
|
124
134
|
config.rest = { port: Number(restPort), host: restHost, token: restToken };
|
|
125
135
|
}
|
|
136
|
+
if (oauthPort) {
|
|
137
|
+
// the other oauth options (clients, scopes ...) come from the --config file
|
|
138
|
+
config.oauth = Object.assign({}, config.oauth, { port: Number(oauthPort) });
|
|
139
|
+
for (const [key, value] of Object.entries({ host: oauthHost, consent: oauthConsent, issuer: oauthIssuer })) {
|
|
140
|
+
if (value) {
|
|
141
|
+
config.oauth[key] = value;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (oauthSecure) {
|
|
145
|
+
config.oauth.secure = true;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
126
148
|
if (smtpPort) {
|
|
127
149
|
// smtp-server is an optional dependency, start() loads it only when SMTP is enabled
|
|
128
150
|
config.smtp = { port: Number(smtpPort) };
|
|
@@ -146,6 +168,19 @@ if (argv.help) {
|
|
|
146
168
|
if (server.restServer) {
|
|
147
169
|
console.log('REST API listening on %s:%s', (config.rest && config.rest.host) || '127.0.0.1', server.restServer.address().port);
|
|
148
170
|
}
|
|
171
|
+
if (server.oauthServer) {
|
|
172
|
+
const clients = server.control.listOAuthClients().map(client => client.id);
|
|
173
|
+
const consent = server.oauth.consent;
|
|
174
|
+
console.log(
|
|
175
|
+
'OAuth provider listening on %s (clients: %s; default consent: %s)',
|
|
176
|
+
server.oauth.issuer,
|
|
177
|
+
clients.join(', '),
|
|
178
|
+
typeof consent === 'string' ? consent : 'error ' + consent.error
|
|
179
|
+
);
|
|
180
|
+
if (!server.capabilities['AUTH=XOAUTH2'] && !server.capabilities['AUTH=OAUTHBEARER']) {
|
|
181
|
+
console.log('Load the XOAUTH2 or OAUTHBEARER plugin (with SASL-IR) to log in to IMAP with its tokens');
|
|
182
|
+
}
|
|
183
|
+
}
|
|
149
184
|
if (server.smtpServer) {
|
|
150
185
|
console.log('Incoming SMTP server up and running on port %s', server.smtpServer.server.address().port);
|
|
151
186
|
}
|