imapkit 4.3.0 → 5.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/README.md +167 -51
- package/bin/help.txt +17 -3
- package/bin/imapkit.js +53 -23
- package/dist/cjs/commands/append.js +1 -1
- package/dist/cjs/commands/create.js +2 -2
- package/dist/cjs/commands/delete.js +2 -14
- package/dist/cjs/commands/handlers/flags.d.ts +3 -3
- package/dist/cjs/commands/handlers/flags.js +3 -3
- package/dist/cjs/commands/handlers/store.js +3 -3
- package/dist/cjs/commands/rename.js +6 -46
- package/dist/cjs/commands/select.js +1 -0
- package/dist/cjs/commands/subscribe.js +7 -8
- package/dist/cjs/commands/unsubscribe.js +3 -9
- package/dist/cjs/control.d.ts +430 -0
- package/dist/cjs/control.js +637 -0
- package/dist/cjs/index.d.ts +8 -1
- package/dist/cjs/index.js +9 -2
- package/dist/cjs/load-plugins.d.ts +1 -1
- package/dist/cjs/load-plugins.js +7 -2
- package/dist/cjs/plugins/acl.js +109 -42
- package/dist/cjs/plugins/condstore.js +7 -0
- package/dist/cjs/plugins/index.js +1 -3
- package/dist/cjs/plugins/metadata.js +140 -57
- package/dist/cjs/plugins/objectid.js +10 -0
- package/dist/cjs/plugins/quota.js +50 -2
- package/dist/cjs/plugins/savedate.js +1 -1
- package/dist/cjs/plugins/special-use.d.ts +0 -8
- package/dist/cjs/plugins/special-use.js +23 -0
- package/dist/cjs/quirks.d.ts +28 -0
- package/dist/cjs/quirks.js +124 -0
- package/dist/cjs/random.d.ts +10 -0
- package/dist/cjs/random.js +22 -0
- package/dist/cjs/rest.d.ts +50 -0
- package/dist/cjs/rest.js +561 -0
- package/dist/cjs/script.d.ts +33 -4
- package/dist/cjs/script.js +64 -4
- package/dist/cjs/server.d.ts +149 -8
- package/dist/cjs/server.js +429 -152
- package/dist/cjs/smtp-listener.d.ts +3 -2
- package/dist/cjs/smtp-listener.js +10 -6
- package/dist/cjs/storage-schema.d.ts +139 -0
- package/dist/cjs/storage-schema.js +215 -0
- package/dist/cjs/store-operations.d.ts +168 -0
- package/dist/cjs/store-operations.js +422 -0
- package/dist/cjs/types.d.ts +22 -1
- package/dist/esm/commands/append.js +1 -1
- package/dist/esm/commands/create.js +2 -2
- package/dist/esm/commands/delete.js +2 -14
- package/dist/esm/commands/handlers/flags.d.ts +3 -3
- package/dist/esm/commands/handlers/flags.js +3 -3
- package/dist/esm/commands/handlers/store.js +3 -3
- package/dist/esm/commands/rename.js +6 -46
- package/dist/esm/commands/select.js +1 -0
- package/dist/esm/commands/subscribe.js +7 -8
- package/dist/esm/commands/unsubscribe.js +3 -9
- package/dist/esm/control.d.ts +430 -0
- package/dist/esm/control.js +630 -0
- package/dist/esm/index.d.ts +8 -1
- package/dist/esm/index.js +5 -2
- package/dist/esm/load-plugins.d.ts +1 -1
- package/dist/esm/load-plugins.js +7 -2
- package/dist/esm/plugins/acl.js +109 -42
- package/dist/esm/plugins/condstore.js +7 -0
- package/dist/esm/plugins/index.js +1 -3
- package/dist/esm/plugins/metadata.js +140 -57
- package/dist/esm/plugins/objectid.js +10 -0
- package/dist/esm/plugins/quota.js +50 -2
- package/dist/esm/plugins/savedate.js +1 -1
- package/dist/esm/plugins/special-use.d.ts +0 -8
- package/dist/esm/plugins/special-use.js +23 -0
- package/dist/esm/quirks.d.ts +28 -0
- package/dist/esm/quirks.js +120 -0
- package/dist/esm/random.d.ts +10 -0
- package/dist/esm/random.js +19 -0
- package/dist/esm/rest.d.ts +50 -0
- package/dist/esm/rest.js +553 -0
- package/dist/esm/script.d.ts +33 -4
- package/dist/esm/script.js +63 -4
- package/dist/esm/server.d.ts +149 -8
- package/dist/esm/server.js +429 -152
- package/dist/esm/smtp-listener.d.ts +3 -2
- package/dist/esm/smtp-listener.js +10 -6
- package/dist/esm/storage-schema.d.ts +139 -0
- package/dist/esm/storage-schema.js +211 -0
- package/dist/esm/store-operations.d.ts +168 -0
- package/dist/esm/store-operations.js +407 -0
- package/dist/esm/types.d.ts +22 -1
- package/dist/plugin-help.json +0 -14
- package/package.json +11 -3
- package/dist/cjs/plugins/xtoybird.d.ts +0 -2
- package/dist/cjs/plugins/xtoybird.js +0 -236
- package/dist/esm/plugins/xtoybird.d.ts +0 -2
- package/dist/esm/plugins/xtoybird.js +0 -218
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ Homepage: [imapkit.com](https://imapkit.com). ImapKit requires Node.js 20 or new
|
|
|
12
12
|
|
|
13
13
|
> ImapKit is maintained by the team behind **[EmailEngine](https://emailengine.app/?utm_source=imapkit-readme&utm_medium=readme&utm_campaign=oss-docs&utm_content=note)**, a self-hosted email API that turns Gmail, Microsoft 365, and IMAP accounts into REST endpoints, with managed OAuth2 and webhooks for incoming mail. If you need a production email integration rather than a mock IMAP server for tests, start there.
|
|
14
14
|
|
|
15
|
-
> **Formerly Hoodiecrow.** ImapKit was published as [`hoodiecrow-imap`](https://www.npmjs.com/package/hoodiecrow-imap) up to version 3.3.1. To migrate, install `imapkit` instead and use `require('imapkit')`. The command is now `imapkit` and its environment variables start with `IMAPKIT_` instead of `HOODIECROW_`. The API, plugins
|
|
15
|
+
> **Formerly Hoodiecrow.** ImapKit was published as [`hoodiecrow-imap`](https://www.npmjs.com/package/hoodiecrow-imap) up to version 3.3.1. To migrate, install `imapkit` instead and use `require('imapkit')`. The command is now `imapkit` and its environment variables start with `IMAPKIT_` instead of `HOODIECROW_`. The API, plugins and storage format are unchanged.
|
|
16
16
|
|
|
17
17
|
# Usage
|
|
18
18
|
|
|
@@ -27,7 +27,7 @@ imapkit -p 1143
|
|
|
27
27
|
|
|
28
28
|
Point your IMAP client to `localhost:1143` and log in with user name `testuser` and password `testpass`. Without `-p` the server listens on port 143 (993 with `--secure`), which usually needs root privileges.
|
|
29
29
|
|
|
30
|
-
`imapkit --smtpPort=1025` also starts an SMTP server that appends every message it receives to INBOX.
|
|
30
|
+
`imapkit --smtpPort=1025` also starts an SMTP server that appends every message it receives to INBOX. SMTP needs the optional [smtp-server](https://www.npmjs.com/package/smtp-server) package, which is not installed with ImapKit: `npm install -g smtp-server` (or `npm install smtp-server` next to a local install).
|
|
31
31
|
|
|
32
32
|
Run `imapkit --help` to see all command line options, the environment variables (`IMAPKIT_PORT`, `IMAPKIT_PLUGINS`, ...) and sample configuration data. For example, `imapkit -p 1143 --plugin=IDLE,MOVE,CONDSTORE --storage=storage.json` loads three plugins and the mailboxes of `storage.json`.
|
|
33
33
|
|
|
@@ -57,7 +57,7 @@ import imapkit, { type IMAPServerOptions, type Plugin } from 'imapkit';
|
|
|
57
57
|
|
|
58
58
|
ImapKit needs Node.js 20 or newer. It also runs on the latest [Bun](https://bun.sh/) and [Deno](https://deno.com/) releases (`npm:imapkit` in Deno), CI runs the whole test suite on both.
|
|
59
59
|
|
|
60
|
-
See [complete.js](https://github.com/postalsys/imapkit/blob/master/examples/complete.js) for an example.
|
|
60
|
+
See [complete.js](https://github.com/postalsys/imapkit/blob/master/examples/complete.js) for an example, [control-api.js](https://github.com/postalsys/imapkit/blob/master/examples/control-api.js) for a client test that changes the server state with the control API, and [rest-api.py](https://github.com/postalsys/imapkit/blob/master/examples/rest-api.py) for the REST API from Python.
|
|
61
61
|
|
|
62
62
|
## Scope
|
|
63
63
|
|
|
@@ -119,7 +119,7 @@ ImapKit follows these of the strategies that RFC 2180 (IMAP4 Multi-Accessed Mail
|
|
|
119
119
|
|
|
120
120
|
## Authentication
|
|
121
121
|
|
|
122
|
-
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 `
|
|
122
|
+
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.
|
|
123
123
|
|
|
124
124
|
## Status
|
|
125
125
|
|
|
@@ -194,7 +194,6 @@ An unknown plugin name throws an error, and a plugin listed more than once is lo
|
|
|
194
194
|
- **UTF8=ACCEPT** Adds UTF8=ACCEPT [RFC9755] capability and loads ENABLE. After `ENABLE UTF8=ACCEPT` mailbox names are UTF-8 in both directions (storage keeps modified UTF-7 names, so `&` is an ordinary character), strings that are valid UTF-8 are sent quoted, and SEARCH strings are UTF-8 without `CHARSET`. UTF8=ONLY, the obsolete `APPEND ... UTF8 (...)` data item of RFC 6855 and downgrading of 8-bit headers for clients that did not enable UTF-8 (RFC 9755 section 8) are not implemented
|
|
195
195
|
- **X-GM-EXT-1** Adds [Gmail specific](https://developers.google.com/workspace/gmail/imap/imap-extensions) extensions. `X-GM-MSGID` and `X-GM-THRID` work with FETCH and SEARCH (every message is its own thread unless the storage sets an `X-GM-THRID` value for it; with OBJECTID loaded, the messages of a `THREADID` share the `X-GM-THRID` of the first message of that thread, so both thread ids group the same messages). `X-GM-LABELS` works with FETCH, STORE (`+`, `-`, `.SILENT`) and SEARCH: system labels are atoms that start with `\` (`\Inbox` for INBOX, the special-use attribute for special-use mailboxes), other labels are mailbox names, sent and read in the form the session uses for mailbox names (modified UTF-7, or UTF-8 after `ENABLE UTF8=ACCEPT`) and quoted when they are not atoms. In SEARCH a label that starts with `\` is a system label. Setting a label does not change message behavior, for example the message does not get copied to another mailbox. `X-GM-RAW` supports a subset of the Gmail search syntax: words and `"phrases"` (TEXT), `-term`, `OR`, `( )`, `{ }`, `from:`, `to:`, `cc:`, `bcc:`, `subject:`, `label:`, `in:` (`inbox`, `sent`, `drafts`, `trash`, `spam`, `anywhere` or a label), `is:` (`read`, `unread`, `starred`, `important`), `larger:` and `smaller:` (with `k` or `m`), `after:` and `before:` (`YYYY/MM/DD`) and `rfc822msgid:`. Other Gmail operators (`has:`, `older_than:` ...) are answered with NO
|
|
196
196
|
- **XOAUTH2** Gmail XOAUTH2 login. Needs SASL-IR (load the SASL-IR plugin too), Gmail itself does not. Use `"testuser"` as the user name and `"testtoken"` as the access token to log in.
|
|
197
|
-
- **XTOYBIRD** Custom plugin to allow programmatic control of the server. XTOYBIRD commands are only allowed after login
|
|
198
197
|
|
|
199
198
|
## ACL
|
|
200
199
|
|
|
@@ -222,43 +221,23 @@ The rights of other users are enforced as RFC 4314 section 4 describes:
|
|
|
222
221
|
|
|
223
222
|
Missing rights are answered with `NO [NOPERM]`, or with the same error as for a mailbox that does not exist when the user does not have `l` either, so the existence of the mailbox is not disclosed (RFC 4314 section 6). The rights on the selected mailbox are taken when it is selected. A new mailbox inherits the ACL of its parent and DELETE removes the ACL. The obsolete `c` and `d` rights are accepted as `kx` and `et` and are added to ACL and MYRIGHTS responses (RFC 4314 section 2.1.1). The rights of the owner can not be changed.
|
|
224
223
|
|
|
225
|
-
##
|
|
224
|
+
## Migrating from 4.x
|
|
226
225
|
|
|
227
|
-
|
|
226
|
+
ImapKit 5.0.0 has two breaking changes:
|
|
228
227
|
|
|
229
|
-
|
|
228
|
+
- SMTP (`--smtpPort`, the `smtp` option) needs the `smtp-server` package, it is an optional peer dependency now: `npm install smtp-server`.
|
|
229
|
+
- The XTOYBIRD plugin was removed, the IMAP port carries IMAP traffic only. Loading `XTOYBIRD` throws an error that points here.
|
|
230
230
|
|
|
231
|
-
|
|
231
|
+
The XTOYBIRD commands map to the [control API](#control-api):
|
|
232
232
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
```
|
|
243
|
-
S: * OK ImapKit ready for rumble
|
|
244
|
-
C: A0 LOGIN testuser testpass
|
|
245
|
-
S: A0 OK User logged in
|
|
246
|
-
C: A1 XTOYBIRD STORAGE
|
|
247
|
-
S: * XTOYBIRD [XJSONDUMP] {3224}
|
|
248
|
-
S: {
|
|
249
|
-
S: "INBOX": {
|
|
250
|
-
S: "messages": [
|
|
251
|
-
S: {
|
|
252
|
-
S: "raw": "Subject: hello 1\r\n\r\nWorld 1!",
|
|
253
|
-
S: ...
|
|
254
|
-
S: A1 OK XTOYBIRD Completed
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
## Ideas for future XTOYBIRD commands
|
|
258
|
-
|
|
259
|
-
- Change UIDVALIDITY at runtime (eg. `A1 XTOYBIRD UIDVALIDITY INBOX 123` where 123 is the new UIDVALIDITY for INBOX)
|
|
260
|
-
- Reset the server to its initial state (`A1 XTOYBIRD RESET`)
|
|
261
|
-
- Replace the storage at runtime with a JSON string that describes the entire storage (`A1 XTOYBIRD UPDATE {123}\r\n{"INBOX":{...}})`)
|
|
233
|
+
| XTOYBIRD command | Control API |
|
|
234
|
+
| ------------------------------------ | -------------------------------------------------------------------------------------------- |
|
|
235
|
+
| `XTOYBIRD STORAGE` | `server.control.snapshot()` |
|
|
236
|
+
| `XTOYBIRD SERVER` | `server.control.listMailboxes()`, `server.control.listUsers()` |
|
|
237
|
+
| `XTOYBIRD CONNECTION` | `server.control.sessions()` |
|
|
238
|
+
| `XTOYBIRD USERADD "user" "password"` | `server.control.addUser('user', { password })`, or `updateUser()` for an existing user |
|
|
239
|
+
| `XTOYBIRD USERDEL "user"` | `server.control.deleteUser('user')` |
|
|
240
|
+
| `XTOYBIRD SHUTDOWN` | `await server.control.shutdown()`, stops accepting connections and waits for the last client |
|
|
262
241
|
|
|
263
242
|
## CONDSTORE support
|
|
264
243
|
|
|
@@ -395,6 +374,117 @@ describe('IMAP tests', () => {
|
|
|
395
374
|
});
|
|
396
375
|
```
|
|
397
376
|
|
|
377
|
+
## Control API
|
|
378
|
+
|
|
379
|
+
`server.control` changes and inspects the server from your test, without an IMAP session. Every change reaches the connected sessions the way a change by another session would: a selected session gets `EXISTS` for a new message, `EXPUNGE` (or `VANISHED` after `ENABLE QRESYNC`) for a removed one, an unsolicited `FETCH` with the UID and the new flags (with `MODSEQ` after `ENABLE CONDSTORE`), and `BYE` when its mailbox is deleted. NOTIFY and CONTEXT=SEARCH sessions get their updates too. ACL does not apply to the control API, but every argument is checked.
|
|
380
|
+
|
|
381
|
+
```javascript
|
|
382
|
+
const server = imapkit({ plugins: ['IDLE', 'CONDSTORE'] });
|
|
383
|
+
const port = await server.start(); // a free port, server.start(1143, '127.0.0.1') for a fixed one
|
|
384
|
+
|
|
385
|
+
const { uid } = server.control.addMessage('INBOX', { raw: 'Subject: hello\r\n\r\nHi!\r\n', flags: ['\\Seen'] });
|
|
386
|
+
server.control.setFlags('INBOX', [uid], ['\\Flagged'], 'add');
|
|
387
|
+
server.control.expungeMessages('INBOX', [uid]);
|
|
388
|
+
|
|
389
|
+
await server.stop();
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
Mailboxes are addressed by their storage name (modified UTF-7, the name a client uses in LIST), messages by mailbox and UID. The methods return plain data and throw an `ImapKitError` (exported by the package) whose `code` is `NONEXISTENT`, `ALREADYEXISTS`, `INVALID`, or the RFC 5530 code of a failed mailbox operation (`CANNOT`, `HASCHILDREN` ...).
|
|
393
|
+
|
|
394
|
+
| Method | What it does |
|
|
395
|
+
| ---------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
396
|
+
| `snapshot()` | The storage as JSON in the shape of the `storage` option, `imapkit({ storage: server.control.snapshot() })` starts from the same state |
|
|
397
|
+
| `listMailboxes()`, `getMailbox(path)` | Mailboxes with `path`, `delimiter`, `flags`, `selectable`, `subscribed`, `messages`, `unseen`, `uidnext`, `uidvalidity`, `permanentFlags` (and `highestModseq` with CONDSTORE) |
|
|
398
|
+
| `listMessages(path, { uids, raw })`, `getMessage(path, uid)` | Messages with `uid`, `flags`, `internaldate`, `size` (and `modseq` with CONDSTORE), the source as a Buffer in `raw` |
|
|
399
|
+
| `sessions()` | Connected sessions: `session` number, `user`, `state`, selected `mailbox`, `readOnly`, `enabled`, `secure`, `compressed`, `remoteAddress` |
|
|
400
|
+
| `addMessage(path, { raw, flags, internaldate }, { checks })` | Adds a message like a delivery, returns `{ uid, uidvalidity }`. A string `raw` is encoded as UTF-8. `checks: true` refuses the message like APPEND would (QUOTA `OVERQUOTA`, APPENDLIMIT `TOOBIG`) |
|
|
401
|
+
| `setFlags(path, uids, flags, mode)` | `mode` is `set` (default), `add` or `remove` |
|
|
402
|
+
| `expungeMessages(path, uids)` | Removes messages |
|
|
403
|
+
| `copyMessages(path, uids, target)`, `moveMessages(...)` | Returns `{ uidvalidity, uids: [{ uid, targetUid }] }` |
|
|
404
|
+
| `replaceMessage(path, uid, { raw, flags, internaldate })` | Adds the new message and expunges the old one. The content of a UID never changes (RFC 9051 section 2.3.1.1), so the new message gets a new UID |
|
|
405
|
+
| `createMailbox(path, { subscribed })`, `deleteMailbox(path)`, `renameMailbox(path, newPath)` | Like CREATE, DELETE and RENAME |
|
|
406
|
+
| `resetUidValidity(path, { uidvalidity, uids, offset, seed })` | Gives the mailbox a new, greater UIDVALIDITY. `uids` is `keep` (default), `renumber` (1 to n), `shuffle` (1 to n in a random order, repeatable with `seed`, so an old UID points to another message) or `offset` (every UID moves above the old UIDNEXT, plus `offset`, so old UIDs find nothing). A UID must not change during a session (RFC 9051 section 2.3.1.1), so sessions that have the mailbox selected get `BYE`. Returns `{ uidvalidity, uidnext, uids: [{ uid, newUid }] }` |
|
|
407
|
+
| `subscribe(path)`, `unsubscribe(path)` | Return true if the subscription changed |
|
|
408
|
+
| `listUsers()`, `addUser(name, { password, xoauth2 })`, `updateUser(name, {...})`, `deleteUser(name)` | `xoauth2` is `{ accessToken, sessionTimeout }`. Deleting a user disconnects its sessions, `{ disconnect: false }` keeps them. No credentials in `listUsers()` |
|
|
409
|
+
| `disconnect(session or { user }, { text, reset })` | Disconnects sessions with an untagged `BYE`, or resets the TCP connection |
|
|
410
|
+
| `inject(session, data)` | Writes bytes to a session as they are, e.g. `* OK [ALERT] ...` between commands |
|
|
411
|
+
| `reset()` | Restores the mailboxes and users of the server options and disconnects every session with `BYE`, so a long running server can be reused between tests. Script rules stay |
|
|
412
|
+
| `shutdown({ graceful })` | Stops accepting connections and resolves once the last client is gone, `graceful: false` closes the sessions |
|
|
413
|
+
|
|
414
|
+
Plugins add operations for their own data, they exist only when the plugin is loaded:
|
|
415
|
+
|
|
416
|
+
| Plugin | Methods |
|
|
417
|
+
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
418
|
+
| ACL | `getAcl(path)`, `setAcl(path, identifier, rights)` (`+rights` adds, `-rights` removes, empty rights remove the identifier), `deleteAcl(path, identifier)`, ACLs are `{ identifier: rights }` |
|
|
419
|
+
| QUOTA | `getQuota()` (`{ root, limits, usage }`, STORAGE in units of 1024 octets), `setQuota({ STORAGE, MESSAGE, MAILBOX })` replaces the limits |
|
|
420
|
+
| METADATA, METADATA-SERVER | `getMetadata(path)`, `setMetadata(path, { entry: value })` (`""` is the server, null removes an entry, also the read-only `/shared/admin`). Sessions after `ENABLE METADATA` get unsolicited METADATA |
|
|
421
|
+
| SPECIAL-USE | `setSpecialUse(path, ['\\Sent'])` (RFC 6154 attributes and `\Important`), `getMailbox()` has `specialUse` |
|
|
422
|
+
| OBJECTID | `getMailbox()` has `mailboxId`, `getMessage()` and `listMessages()` have `emailId` and `threadId` |
|
|
423
|
+
|
|
424
|
+
A plugin of your own adds operations with `server.control.register(name, fn, routes)`, the routes go to the REST API.
|
|
425
|
+
|
|
426
|
+
`server.start(port, host)` resolves with the port, `server.stop()` closes the server and every session. `listen()` and `close()` still work. With the `smtp` option (`imapkit({ smtp: { port, host } })`) `start()` also starts an SMTP server that appends every message it receives to INBOX, `server.smtpServer` is that server. It needs the optional `smtp-server` package (`npm install smtp-server`), `start()` rejects with an error that says so when it is missing.
|
|
427
|
+
|
|
428
|
+
### Events
|
|
429
|
+
|
|
430
|
+
Tests can wait for events instead of polling:
|
|
431
|
+
|
|
432
|
+
- `session`: `{ type, session }`, `type` is `open`, `login`, `select` (also when the same mailbox is selected again), `unselect` (CLOSE, UNSELECT, a failed SELECT, BYE), `logout` (UNAUTHENTICATE), `waiting` (a command waits for client input, with `command`, e.g. `IDLE` after its continuation) or `close`, `session` is the session as `sessions()` describes it.
|
|
433
|
+
- `command`: `{ session, tag, command, status, user }` when the tagged response of a command goes out, `status` is `OK`, `NO` or `BAD`.
|
|
434
|
+
- `mailbox`: `{ type, path, oldPath, mailbox, origin }` for CREATE, DELETE, RENAME, SUBSCRIBE and UNSUBSCRIBE, `origin` is null for the control API.
|
|
435
|
+
- `expunge`: `(mailbox, messages, origin)` before the sessions are told, and `flags`: `(mailbox, messages, origin)` for flag changes of the control API.
|
|
436
|
+
|
|
437
|
+
```javascript
|
|
438
|
+
const idling = new Promise(resolve => server.on('session', event => event.type === 'waiting' && event.command === 'IDLE' && resolve(event)));
|
|
439
|
+
// ... let the client start IDLE ...
|
|
440
|
+
await idling;
|
|
441
|
+
server.control.addMessage('INBOX', { raw: message }); // the idling client gets * n EXISTS right away
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
## REST API
|
|
445
|
+
|
|
446
|
+
The REST API is the control API over HTTP, for test suites in any language. It is off unless the `rest` option (`imapkit({ rest: { port, host, token } })` with `server.start()`) or `--rest-port` turns it on:
|
|
447
|
+
|
|
448
|
+
```bash
|
|
449
|
+
imapkit -p 1143 --rest-port=8143
|
|
450
|
+
curl -X POST http://127.0.0.1:8143/v1/mailboxes/INBOX/messages \
|
|
451
|
+
-H 'Content-Type: application/json' \
|
|
452
|
+
-d '{"raw": "Subject: hello\r\n\r\nHi!\r\n", "flags": ["\\Seen"]}'
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
The REST API controls the whole server, including the mail of every user, so it is careful by default. It listens on `127.0.0.1` unless `host` (`--rest-host`) says otherwise, and any other address than a loopback one needs a bearer token (`token`, `--rest-token`, `IMAPKIT_REST_TOKEN`) that every request sends as `Authorization: Bearer <token>`. Without a token it only answers requests for a loopback host name. It sends no CORS headers and takes JSON bodies only (`Content-Type: application/json`, at most 64 MiB), so a web page can not call it. Do not expose it outside a test environment.
|
|
456
|
+
|
|
457
|
+
Requests and responses are JSON. Errors are `{ "error": { "code", "message" } }` with HTTP status 404 for `NONEXISTENT`, 409 for `ALREADYEXISTS` and the RFC 5530 codes of a failed mailbox operation, 400 for `INVALID`, 401 for a missing or wrong token. 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.
|
|
458
|
+
|
|
459
|
+
`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`, `expunge`, `flags`, `acl`, `script`, `reset`), mailboxes are paths, messages UIDs and sessions numbers in the data:
|
|
460
|
+
|
|
461
|
+
```bash
|
|
462
|
+
curl -N 'http://127.0.0.1:8143/v1/events?types=session'
|
|
463
|
+
# event: session
|
|
464
|
+
# data: {"type":"select","session":{"session":1,"user":"testuser","state":"Selected","mailbox":"INBOX",...}}
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
| Endpoint | Control API |
|
|
468
|
+
| ---------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
|
|
469
|
+
| `GET /v1/snapshot` | `snapshot()` |
|
|
470
|
+
| `POST /v1/reset` | `reset()` |
|
|
471
|
+
| `POST /v1/shutdown` (`{ graceful }`) | `shutdown()` |
|
|
472
|
+
| `GET /v1/sessions`, `DELETE /v1/sessions/{n}` (`{ text, reset }`) | `sessions()`, `disconnect()` |
|
|
473
|
+
| `POST /v1/sessions/{n}/inject` (`{ data, encoding }`) | `inject()` |
|
|
474
|
+
| `GET`, `POST /v1/users`, `PUT`, `DELETE /v1/users/{name}` | `listUsers()`, `addUser()`, `updateUser()`, `deleteUser()` |
|
|
475
|
+
| `GET`, `POST /v1/mailboxes`, `GET`, `DELETE /v1/mailboxes/{path}` | `listMailboxes()`, `createMailbox()`, `getMailbox()`, `deleteMailbox()` |
|
|
476
|
+
| `POST /v1/mailboxes/{path}/rename` (`{ newPath }`) | `renameMailbox()` |
|
|
477
|
+
| `PUT`, `DELETE /v1/mailboxes/{path}/subscription` | `subscribe()`, `unsubscribe()` |
|
|
478
|
+
| `POST /v1/mailboxes/{path}/uidvalidity` | `resetUidValidity()` |
|
|
479
|
+
| `GET /v1/mailboxes/{path}/messages` (`?uids=1,2&raw=true`), `POST` (`{ raw, flags }`) | `listMessages()`, `addMessage()` |
|
|
480
|
+
| `GET`, `DELETE /v1/mailboxes/{path}/messages/{uid}` | `getMessage()`, `expungeMessages()` |
|
|
481
|
+
| `POST /v1/mailboxes/{path}/messages/flags`, `.../expunge`, `.../copy`, `.../move` | `setFlags()`, `expungeMessages()`, `copyMessages()`, `moveMessages()` |
|
|
482
|
+
| `GET /v1/mailboxes/{path}/acl`, `PUT`, `DELETE /v1/mailboxes/{path}/acl/{identifier}` (`{ rights }`) | `getAcl()`, `setAcl()`, `deleteAcl()` (ACL) |
|
|
483
|
+
| `GET`, `PUT /v1/quota` | `getQuota()`, `setQuota()` (QUOTA) |
|
|
484
|
+
| `GET`, `PUT /v1/metadata`, `GET`, `PUT /v1/mailboxes/{path}/metadata` | `getMetadata()`, `setMetadata()` (METADATA) |
|
|
485
|
+
| `PUT /v1/mailboxes/{path}/special-use` (`{ specialUse }`) | `setSpecialUse()` (SPECIAL-USE) |
|
|
486
|
+
| `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 |
|
|
487
|
+
|
|
398
488
|
## Scripted faults
|
|
399
489
|
|
|
400
490
|
ImapKit is strict and correct by default. To test how a client copes with a server that is not, script rules make the server deviate from the protocol at chosen points: answer a command with a canned response, send a literal where a quoted string is expected, cut a response in the middle of a literal, delay or split output, or drop the connection. Rules come from the `script` server option (a rule or a list of rules), or are added at runtime with `server.script.add()`:
|
|
@@ -429,21 +519,23 @@ Every rule watches one event (`on`):
|
|
|
429
519
|
- `input`: a line read by a command that takes over the input, like `DONE` of IDLE or a SASL response of AUTHENTICATE
|
|
430
520
|
- `response`: every response the server sends with `connection.send()`, tagged and untagged, as the exact bytes that are about to go out, after every plugin and the core changed the response
|
|
431
521
|
- `continuation`: a `+` continuation request (literals, IDLE, AUTHENTICATE)
|
|
522
|
+
- `quiet`: the session had no input and no output for `quietFor` milliseconds (required for this event). The output of the rule starts the next quiet time. During IDLE the event belongs to the IDLE command, so `{ on: 'quiet', command: 'IDLE', quietFor: 1800000, send: '* BYE Autologout; idle for too long\r\n', close: true }` is an autologout, and `{ on: 'quiet', state: 'Selected', quietFor: 500, times: 1, send: '* OK [ALERT] System shutdown in 10 minutes\r\n' }` an ALERT between commands. A quiet rule can `send` and `close`
|
|
432
523
|
|
|
433
524
|
The matchers of a rule all have to match. Rules are checked in the order they were added, the first rule that matches and is not used up handles the event, so a later rule can handle what an earlier one leaves alone.
|
|
434
525
|
|
|
435
|
-
| Matcher | Events | Matches
|
|
436
|
-
| -------------------------- | ---------------------- |
|
|
437
|
-
| `command` | all but greeting | the command name, or a list of names, case-insensitive (`'UID FETCH'`). An unsolicited response belongs to the command that runs, or that reads input (IDLE)
|
|
438
|
-
| `tag` | all but greeting | the command tag, a string or a RegExp
|
|
439
|
-
| `description` | response, continuation | the description passed to `connection.send()`, or a list of them. Continuation requests are `LITERAL`, `IDLE`, `AUTHENTICATE PLAIN` and `AUTHENTICATE OAUTHBEARER`
|
|
440
|
-
| `untagged` | response | `true` for untagged responses only, `false` for tagged ones
|
|
441
|
-
| `session` | all | the number of the connection, or a list of numbers, 1 for the first connection the server accepted
|
|
442
|
-
| `state`, `user`, `mailbox` | all | the session state (`'Not Authenticated'`, `'Authenticated'`, `'Selected'`), the authenticated user, the path of the selected mailbox
|
|
443
|
-
| `match` | all | a RegExp, or a string with a regular expression, tested against the command line or the output bytes (a binary string)
|
|
444
|
-
| `when` | all | a function that gets the event context and returns true to match
|
|
445
|
-
| `nth` | all | the rule fires from the nth matching event on (default 1)
|
|
446
|
-
| `times` | all | the rule fires this many times at most, then lets later rules handle the event
|
|
526
|
+
| Matcher | Events | Matches |
|
|
527
|
+
| -------------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
528
|
+
| `command` | all but greeting | the command name, or a list of names, case-insensitive (`'UID FETCH'`). An unsolicited response belongs to the command that runs, or that reads input (IDLE) |
|
|
529
|
+
| `tag` | all but greeting | the command tag, a string or a RegExp |
|
|
530
|
+
| `description` | response, continuation | the description passed to `connection.send()`, or a list of them. Continuation requests are `LITERAL`, `IDLE`, `AUTHENTICATE PLAIN` and `AUTHENTICATE OAUTHBEARER` |
|
|
531
|
+
| `untagged` | response | `true` for untagged responses only, `false` for tagged ones |
|
|
532
|
+
| `session` | all | the number of the connection, or a list of numbers, 1 for the first connection the server accepted |
|
|
533
|
+
| `state`, `user`, `mailbox` | all | the session state (`'Not Authenticated'`, `'Authenticated'`, `'Selected'`), the authenticated user, the path of the selected mailbox |
|
|
534
|
+
| `match` | all | a RegExp, or a string with a regular expression, tested against the command line or the output bytes (a binary string) |
|
|
535
|
+
| `when` | all | a function that gets the event context and returns true to match |
|
|
536
|
+
| `nth` | all | the rule fires from the nth matching event on (default 1) |
|
|
537
|
+
| `times` | all | the rule fires this many times at most, then lets later rules handle the event |
|
|
538
|
+
| `chance` | all | the rule fires on a matching event with this probability (0 to 1). The random numbers come from the `scriptSeed` option, so a run with the same seed and the same client is repeated exactly |
|
|
447
539
|
|
|
448
540
|
The actions say what happens instead of the usual behavior. Strings are sent as they are (binary strings, one character per octet, or UTF-8 when they have characters above U+00FF) without an added CRLF, and `$TAG` in a string is replaced with the tag of the command. A Buffer is sent as it is, a function gets the event context and returns a string or a Buffer.
|
|
449
541
|
|
|
@@ -457,7 +549,7 @@ The actions say what happens instead of the usual behavior. Strings are sent as
|
|
|
457
549
|
| `defer` | response | holds an untagged response back: `'tagged'` sends it right after the tagged response of its command, `'next'` with the answer to the next command, before its first response. Needs `untagged: true`. `send`, `before` and `after` change the held output, `drop`, `delay`, `chunk`, `truncate` and `close` can not be combined with it. A response that does not belong to a command (one that arrives in IDLE belongs to IDLE) is held for the next tagged response or command. Held responses are dropped when the connection closes |
|
|
458
550
|
| `before`, `after` | output events | bytes sent before or after the output, e.g. an unsolicited response |
|
|
459
551
|
| `delay` | all but input | milliseconds to wait before the output goes out, all later output waits behind it. For a command, the wait before the rule acts or the command runs, later commands wait too |
|
|
460
|
-
| `chunk`, `chunkDelay` | all | write the bytes in pieces of `chunk` octets, `chunkDelay` milliseconds apart (default 10)
|
|
552
|
+
| `chunk`, `chunkDelay` | all | write the bytes in pieces of `chunk` octets, `chunkDelay` milliseconds apart (default 10). `chunkDelay: 0` or `'tick'` sends each piece after the earlier one was handed to the system, on the next event loop turn, so the pieces leave as separate TCP segments without a wall clock delay (on Deno a zero timer, about 2 ms a piece) |
|
|
461
553
|
| `truncate` | all | send only this many octets of the bytes, then close the connection |
|
|
462
554
|
| `close` | all | close the connection after the bytes are sent, `'reset'` destroys the socket instead (a TCP RST where the runtime supports it), 20 ms after the bytes so that the RST does not overtake them. Input that arrives meanwhile is not processed |
|
|
463
555
|
|
|
@@ -478,6 +570,30 @@ The `imapkit` command takes the rules as JSON with `--script=<path>` (or `IMAPKI
|
|
|
478
570
|
|
|
479
571
|
Faults change only the output and the handling of the lines a rule matches, the state of the server stays consistent: a LOGIN answered by a rule with `OK` does not log the session in, and a dropped EXPUNGE response still removes the message. COMPRESS works with delayed output, as the output keeps the compression layer it was sent with. Script rules are for tests only, a rule can send anything.
|
|
480
572
|
|
|
573
|
+
### Quirk presets
|
|
574
|
+
|
|
575
|
+
The `quirks` option (`--quirk` for the `imapkit` command) turns on named presets that make the server behave like a known real server, so a client test reproduces that server's bug in every run, without the server itself. A preset is a set of script rules, after the rules of the `script` option, and plugins it leaves out. The presets are exported as data (`import { quirks } from 'imapkit'`), copy one into your own script rules to adjust it.
|
|
576
|
+
|
|
577
|
+
| Quirk | Behavior |
|
|
578
|
+
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
579
|
+
| `james-fetchgroup` | Apache James FetchGroup: only the first section asked for a part in one FETCH is answered, later ones for the same part are empty (`BODY[2.MIME] BODY[2]` gives a zero-length body) |
|
|
580
|
+
| `james-late-fetch` | Apache James: 1 in 4 FETCH responses come after the tagged OK of their command |
|
|
581
|
+
| `yahoo-quoted-sections` | Yahoo: short body sections (up to 100 octets without line breaks) are quoted strings instead of literals |
|
|
582
|
+
| `m365-throttle` | Microsoft 365: 1 in 10 commands (not LOGOUT) is refused with `BAD Request is throttled. Suggested Backoff Time: 1000 milliseconds` |
|
|
583
|
+
| `no-uidplus`, `no-move` | servers without UIDPLUS or MOVE, the plugins are not loaded even when `plugins` lists them |
|
|
584
|
+
|
|
585
|
+
```javascript
|
|
586
|
+
const server = imapkit({ plugins: ['IDLE', 'MOVE'], quirks: ['james-fetchgroup', 'm365-throttle'], scriptSeed: 42 });
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
## Repeatable tests
|
|
590
|
+
|
|
591
|
+
- `scriptSeed`: the seed of the random numbers of script rules with `chance` and the quirk presets that use them (`--script-seed`).
|
|
592
|
+
- `now`: the time the server uses for the dates it sets itself, the INTERNALDATE of a message without one and SAVEDATE: a Date, a timestamp, or a function that returns one. The dates are formatted in the time zone of the process.
|
|
593
|
+
- `resetUidValidity(path, { uids: 'shuffle', seed })` of the control API.
|
|
594
|
+
|
|
595
|
+
The `storage` option is checked when the server is built: a key that looks like a typo of a known one (`message` for `messages`, `uidValidity`) or a wrong type fails with the path of the problem, e.g. `Invalid storage at "INBOX".messages[2]: unknown key "flag", did you mean "flags"?`. Plugins keep their own data on mailboxes and messages, so other keys are allowed. The package exports the check as `validateStorage(storage)` and the shape as a JSON Schema, `storageSchema`, for editors and fixture tooling. `server.control.snapshot()` returns the same shape.
|
|
596
|
+
|
|
481
597
|
## Creating custom plugins
|
|
482
598
|
|
|
483
599
|
A plugin can be a string as a pointer to a built in plugin or a function. Plugin function is run when the server is created and gets server instance object as an argument.
|
package/bin/help.txt
CHANGED
|
@@ -7,7 +7,8 @@ initial state is always guaranteed when running your tests.
|
|
|
7
7
|
|
|
8
8
|
Additionally ImapKit offers an incoming SMTP server - every mail
|
|
9
9
|
sent to it, is appended to INBOX. To use the incoming SMTP server,
|
|
10
|
-
use smtpPort option.
|
|
10
|
+
use smtpPort option. It needs the smtp-server package, which is not
|
|
11
|
+
installed with ImapKit (npm install -g smtp-server).
|
|
11
12
|
|
|
12
13
|
Usage: imapkit [OPTS]
|
|
13
14
|
|
|
@@ -22,11 +23,24 @@ Usage: imapkit [OPTS]
|
|
|
22
23
|
--plugin=<plugin> Enables a plugin for the server. See below for
|
|
23
24
|
available plugins.
|
|
24
25
|
--smtpPort=<port> Port number for incoming SMTP server. If not set
|
|
25
|
-
SMTP server is not started.
|
|
26
|
+
SMTP server is not started. --smtp-port works too
|
|
27
|
+
--rest-port=<port> Starts the REST API (the control API over HTTP,
|
|
28
|
+
GET /v1/openapi.json lists the endpoints)
|
|
29
|
+
--rest-host=<address> Address of the REST API, defaults to 127.0.0.1.
|
|
30
|
+
Any other than a loopback address needs a token
|
|
31
|
+
--rest-token=<token> Bearer token that REST requests must send
|
|
32
|
+
--quirk=<name> Makes the server behave like a known real server
|
|
33
|
+
(james-fetchgroup, james-late-fetch,
|
|
34
|
+
yahoo-quoted-sections, m365-throttle, no-uidplus,
|
|
35
|
+
no-move), comma separated or repeated
|
|
36
|
+
--script-seed=<number> Seed of the random script rules (chance), so a
|
|
37
|
+
run can be repeated
|
|
26
38
|
|
|
27
39
|
The options can also be set with environment variables: IMAPKIT_PORT,
|
|
28
40
|
IMAPKIT_SECURE, IMAPKIT_DEBUG, IMAPKIT_STORAGE, IMAPKIT_CONFIG,
|
|
29
|
-
IMAPKIT_SCRIPT, IMAPKIT_PLUGINS (comma separated)
|
|
41
|
+
IMAPKIT_SCRIPT, IMAPKIT_PLUGINS (comma separated), IMAPKIT_SMTPPORT,
|
|
42
|
+
IMAPKIT_REST_PORT, IMAPKIT_REST_HOST, IMAPKIT_REST_TOKEN, IMAPKIT_QUIRKS
|
|
43
|
+
and IMAPKIT_SCRIPT_SEED.
|
|
30
44
|
|
|
31
45
|
NB! If port or smtpPort values are below 1024 you most probably need to
|
|
32
46
|
use sudo or run the command with administrator rights.
|
package/bin/imapkit.js
CHANGED
|
@@ -21,25 +21,39 @@ const { values: argv } = parseArgs({
|
|
|
21
21
|
storage: { type: 'string' },
|
|
22
22
|
script: { type: 'string' },
|
|
23
23
|
plugin: { type: 'string', multiple: true },
|
|
24
|
-
smtpPort: { type: 'string' }
|
|
24
|
+
smtpPort: { type: 'string' },
|
|
25
|
+
'smtp-port': { type: 'string' },
|
|
26
|
+
'rest-port': { type: 'string' },
|
|
27
|
+
'rest-host': { type: 'string' },
|
|
28
|
+
'rest-token': { type: 'string' },
|
|
29
|
+
quirk: { type: 'string', multiple: true },
|
|
30
|
+
'script-seed': { type: 'string' }
|
|
25
31
|
}
|
|
26
32
|
});
|
|
27
33
|
|
|
28
34
|
const isTrue = value => (value || '').toString().trim().toLowerCase() === 'true';
|
|
29
35
|
|
|
36
|
+
// a repeated option or a comma separated list, e.g. --plugin=IDLE,MOVE --plugin=ID
|
|
37
|
+
const listOption = value =>
|
|
38
|
+
[]
|
|
39
|
+
.concat(value || [])
|
|
40
|
+
.flatMap(item =>
|
|
41
|
+
String(item)
|
|
42
|
+
.trim()
|
|
43
|
+
.split(/\s*,\s*/)
|
|
44
|
+
)
|
|
45
|
+
.filter(Boolean);
|
|
46
|
+
|
|
30
47
|
const configLocation = argv.config || process.env.IMAPKIT_CONFIG;
|
|
31
48
|
const storageLocation = argv.storage || process.env.IMAPKIT_STORAGE;
|
|
32
49
|
const scriptLocation = argv.script || process.env.IMAPKIT_SCRIPT;
|
|
33
|
-
const smtpPort = argv.smtpPort || process.env.IMAPKIT_SMTPPORT;
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
.split(/\s*,\s*/)
|
|
41
|
-
)
|
|
42
|
-
.filter(Boolean);
|
|
50
|
+
const smtpPort = argv.smtpPort || argv['smtp-port'] || process.env.IMAPKIT_SMTPPORT;
|
|
51
|
+
const restPort = argv['rest-port'] || process.env.IMAPKIT_REST_PORT;
|
|
52
|
+
const restHost = argv['rest-host'] || process.env.IMAPKIT_REST_HOST;
|
|
53
|
+
const restToken = argv['rest-token'] || process.env.IMAPKIT_REST_TOKEN;
|
|
54
|
+
const quirksList = listOption(argv.quirk || process.env.IMAPKIT_QUIRKS);
|
|
55
|
+
const scriptSeed = argv['script-seed'] || process.env.IMAPKIT_SCRIPT_SEED;
|
|
56
|
+
const pluginsList = listOption(argv.plugin || process.env.IMAPKIT_PLUGINS).map(plugin => plugin.toUpperCase());
|
|
43
57
|
const secure = isTrue(argv.secure || process.env.IMAPKIT_SECURE);
|
|
44
58
|
const debug = isTrue(argv.debug || process.env.IMAPKIT_DEBUG);
|
|
45
59
|
|
|
@@ -93,18 +107,34 @@ if (argv.help) {
|
|
|
93
107
|
.trim()
|
|
94
108
|
);
|
|
95
109
|
} else {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
|
|
110
|
+
if (quirksList.length) {
|
|
111
|
+
config.quirks = quirksList;
|
|
112
|
+
}
|
|
113
|
+
if (scriptSeed !== undefined) {
|
|
114
|
+
config.scriptSeed = Number(scriptSeed);
|
|
115
|
+
}
|
|
116
|
+
if (restPort) {
|
|
117
|
+
config.rest = { port: Number(restPort), host: restHost, token: restToken };
|
|
118
|
+
}
|
|
106
119
|
if (smtpPort) {
|
|
107
|
-
//
|
|
108
|
-
|
|
120
|
+
// smtp-server is an optional dependency, start() loads it only when SMTP is enabled
|
|
121
|
+
config.smtp = { port: Number(smtpPort) };
|
|
109
122
|
}
|
|
123
|
+
const server = imapkit(config);
|
|
124
|
+
console.log('Starting ImapKit ...');
|
|
125
|
+
server.start(Number(port)).then(
|
|
126
|
+
() => {
|
|
127
|
+
console.log('ImapKit successfully%s listening on port %s', secure ? ' and securely' : '', port);
|
|
128
|
+
if (server.restServer) {
|
|
129
|
+
console.log('REST API listening on %s:%s', restHost || '127.0.0.1', server.restServer.address().port);
|
|
130
|
+
}
|
|
131
|
+
if (server.smtpServer) {
|
|
132
|
+
console.log('Incoming SMTP server up and running on port %s', server.smtpServer.server.address().port);
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
err => {
|
|
136
|
+
console.error('Failed to start ImapKit: %s', err.message);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
);
|
|
110
140
|
}
|
|
@@ -45,7 +45,7 @@ function parseFlags(connection, list) {
|
|
|
45
45
|
throw new Error('Invalid flags argument');
|
|
46
46
|
}
|
|
47
47
|
const value = (0, flags_js_1.normalizeSystemFlag)(flag.value);
|
|
48
|
-
(0, flags_js_1.checkSystemFlags)(connection, value);
|
|
48
|
+
(0, flags_js_1.checkSystemFlags)(connection.server, value);
|
|
49
49
|
return value;
|
|
50
50
|
});
|
|
51
51
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.default = createCommand;
|
|
4
|
+
const store_operations_js_1 = require("../store-operations.js");
|
|
4
5
|
function createCommand(connection, parsed, data, callback) {
|
|
5
6
|
if (!parsed.attributes || parsed.attributes.length !== 1 || !parsed.attributes[0] || ['STRING', 'LITERAL', 'ATOM'].indexOf(parsed.attributes[0].type) < 0) {
|
|
6
7
|
connection.send({
|
|
@@ -18,7 +19,7 @@ function createCommand(connection, parsed, data, callback) {
|
|
|
18
19
|
const path = parsed.attributes[0].value;
|
|
19
20
|
let mailbox;
|
|
20
21
|
try {
|
|
21
|
-
mailbox = connection.server
|
|
22
|
+
mailbox = (0, store_operations_js_1.createMailbox)(connection.server, path);
|
|
22
23
|
}
|
|
23
24
|
catch (err) {
|
|
24
25
|
const E = err;
|
|
@@ -26,7 +27,6 @@ function createCommand(connection, parsed, data, callback) {
|
|
|
26
27
|
connection.sendStatus(parsed, data, 'NO', E.message, E.code, 'CREATE FAILED');
|
|
27
28
|
return callback();
|
|
28
29
|
}
|
|
29
|
-
connection.server.mailboxChanged('create', mailbox.path);
|
|
30
30
|
connection.send({
|
|
31
31
|
tag: parsed.tag,
|
|
32
32
|
command: 'OK',
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.default = deleteCommand;
|
|
4
|
+
const store_operations_js_1 = require("../store-operations.js");
|
|
4
5
|
function deleteCommand(connection, parsed, data, callback) {
|
|
5
6
|
if (!parsed.attributes || parsed.attributes.length !== 1 || !parsed.attributes[0] || ['STRING', 'LITERAL', 'ATOM'].indexOf(parsed.attributes[0].type) < 0) {
|
|
6
7
|
connection.send({
|
|
@@ -16,27 +17,14 @@ function deleteCommand(connection, parsed, data, callback) {
|
|
|
16
17
|
return callback();
|
|
17
18
|
}
|
|
18
19
|
const path = parsed.attributes[0].value;
|
|
19
|
-
const mailbox = connection.server.getMailbox(path);
|
|
20
|
-
if (!mailbox) {
|
|
21
|
-
connection.sendStatus(parsed, data, 'NO', 'Mailbox does not exist', 'NONEXISTENT', 'DELETE FAILED');
|
|
22
|
-
return callback();
|
|
23
|
-
}
|
|
24
20
|
try {
|
|
25
|
-
connection.server
|
|
21
|
+
(0, store_operations_js_1.deleteMailbox)(connection.server, path);
|
|
26
22
|
}
|
|
27
23
|
catch (err) {
|
|
28
24
|
const E = err;
|
|
29
25
|
connection.sendStatus(parsed, data, 'NO', E.message, E.code, 'DELETE FAILED');
|
|
30
26
|
return callback();
|
|
31
27
|
}
|
|
32
|
-
connection.server.mailboxChanged('delete', mailbox.path, { mailbox });
|
|
33
|
-
// RFC 2180 section 3.3: the other sessions that have the mailbox selected are disconnected with an untagged
|
|
34
|
-
// BYE (RFC 2683 section 3.1.2), as they can not be told about the deletion in any other way
|
|
35
|
-
connection.server.connections.forEach(other => {
|
|
36
|
-
if (other !== connection && other.selectedMailbox === mailbox) {
|
|
37
|
-
other.bye('Selected mailbox was deleted', 'MAILBOX DELETED');
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
28
|
connection.send({
|
|
41
29
|
tag: parsed.tag,
|
|
42
30
|
command: 'OK',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { IMAPServer } from '../../types.js';
|
|
2
2
|
declare const INVALID_KEYWORD_CHAR: RegExp;
|
|
3
3
|
/**
|
|
4
4
|
* System flags are case-insensitive, so "\seen" becomes "\Seen"
|
|
@@ -10,8 +10,8 @@ declare function normalizeSystemFlag(flag: string): string;
|
|
|
10
10
|
/**
|
|
11
11
|
* Throws if a flag can not be stored: an unknown system flag (including \Recent) or an invalid keyword
|
|
12
12
|
*
|
|
13
|
-
* @param {Object}
|
|
13
|
+
* @param {Object} server IMAP server, its systemFlags are the system flags that can be stored
|
|
14
14
|
* @param {String} flag Normalized flag value
|
|
15
15
|
*/
|
|
16
|
-
declare function checkSystemFlags(
|
|
16
|
+
declare function checkSystemFlags(server: IMAPServer, flag: string): void;
|
|
17
17
|
export { INVALID_KEYWORD_CHAR, normalizeSystemFlag, checkSystemFlags };
|
|
@@ -24,12 +24,12 @@ function normalizeSystemFlag(flag) {
|
|
|
24
24
|
/**
|
|
25
25
|
* Throws if a flag can not be stored: an unknown system flag (including \Recent) or an invalid keyword
|
|
26
26
|
*
|
|
27
|
-
* @param {Object}
|
|
27
|
+
* @param {Object} server IMAP server, its systemFlags are the system flags that can be stored
|
|
28
28
|
* @param {String} flag Normalized flag value
|
|
29
29
|
*/
|
|
30
|
-
function checkSystemFlags(
|
|
30
|
+
function checkSystemFlags(server, flag) {
|
|
31
31
|
if (flag.charAt(0) === '\\') {
|
|
32
|
-
if (
|
|
32
|
+
if (server.systemFlags.indexOf(flag) < 0) {
|
|
33
33
|
throw new Error('Invalid system flag ' + flag);
|
|
34
34
|
}
|
|
35
35
|
}
|
|
@@ -9,7 +9,7 @@ function setFlags(connection, message, flags) {
|
|
|
9
9
|
const messageFlags = [];
|
|
10
10
|
[].concat(flags).forEach(flag => {
|
|
11
11
|
flag = (0, flags_js_1.normalizeSystemFlag)(typeof flag === 'string' ? flag : String(flag.value || ''));
|
|
12
|
-
(0, flags_js_1.checkSystemFlags)(connection, flag);
|
|
12
|
+
(0, flags_js_1.checkSystemFlags)(connection.server, flag);
|
|
13
13
|
// Ignore if it is not in allowed list and only permament flags are allowed to use
|
|
14
14
|
if (mailbox.permanentFlags.indexOf(flag) < 0 && !mailbox.allowPermanentFlags) {
|
|
15
15
|
return;
|
|
@@ -25,7 +25,7 @@ function addFlags(connection, message, flags) {
|
|
|
25
25
|
const mailbox = connection.selectedMailbox;
|
|
26
26
|
[].concat(flags).forEach(flag => {
|
|
27
27
|
flag = (0, flags_js_1.normalizeSystemFlag)(typeof flag === 'string' ? flag : String(flag.value || ''));
|
|
28
|
-
(0, flags_js_1.checkSystemFlags)(connection, flag);
|
|
28
|
+
(0, flags_js_1.checkSystemFlags)(connection.server, flag);
|
|
29
29
|
// Ignore if it is not in allowed list and only permament flags are allowed to use
|
|
30
30
|
if (mailbox.permanentFlags.indexOf(flag) < 0 && !mailbox.allowPermanentFlags) {
|
|
31
31
|
return;
|
|
@@ -39,7 +39,7 @@ function addFlags(connection, message, flags) {
|
|
|
39
39
|
function removeFlags(connection, message, flags) {
|
|
40
40
|
[].concat(flags).forEach(flag => {
|
|
41
41
|
flag = (0, flags_js_1.normalizeSystemFlag)(typeof flag === 'string' ? flag : String(flag.value || ''));
|
|
42
|
-
(0, flags_js_1.checkSystemFlags)(connection, flag);
|
|
42
|
+
(0, flags_js_1.checkSystemFlags)(connection.server, flag);
|
|
43
43
|
if (message.flags.indexOf(flag) >= 0) {
|
|
44
44
|
for (let i = 0; i < message.flags.length; i++) {
|
|
45
45
|
if (message.flags[i] === flag) {
|