imapkit 4.3.1 → 5.0.1
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 +187 -67
- package/bin/help.txt +20 -5
- package/bin/imapkit.js +64 -24
- 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/fetch.js +2 -1
- 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 +7 -7
- package/dist/cjs/commands/list.js +17 -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 +425 -0
- package/dist/cjs/control.js +637 -0
- package/dist/cjs/dates.d.ts +9 -1
- package/dist/cjs/dates.js +20 -0
- package/dist/cjs/envelope.d.ts +1 -1
- package/dist/cjs/envelope.js +26 -1
- package/dist/cjs/index.d.ts +9 -1
- package/dist/cjs/index.js +9 -2
- package/dist/cjs/load-plugins.d.ts +3 -1
- package/dist/cjs/load-plugins.js +28 -4
- package/dist/cjs/plugins/acl.js +109 -42
- package/dist/cjs/plugins/binary.js +2 -1
- package/dist/cjs/plugins/condstore.js +7 -0
- package/dist/cjs/plugins/context-search.js +2 -2
- package/dist/cjs/plugins/create-special-use.d.ts +0 -5
- package/dist/cjs/plugins/create-special-use.js +11 -1
- package/dist/cjs/plugins/imap4rev2.js +5 -0
- package/dist/cjs/plugins/index.js +1 -3
- package/dist/cjs/plugins/metadata-server.d.ts +6 -2
- package/dist/cjs/plugins/metadata-server.js +3 -1
- package/dist/cjs/plugins/metadata.d.ts +5 -1
- package/dist/cjs/plugins/metadata.js +143 -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 +125 -0
- package/dist/cjs/random.d.ts +10 -0
- package/dist/cjs/random.js +22 -0
- package/dist/cjs/rest.d.ts +54 -0
- package/dist/cjs/rest.js +567 -0
- package/dist/cjs/script.d.ts +33 -4
- package/dist/cjs/script.js +64 -4
- package/dist/cjs/server.d.ts +199 -9
- package/dist/cjs/server.js +603 -227
- 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 +222 -0
- package/dist/cjs/store-operations.d.ts +171 -0
- package/dist/cjs/store-operations.js +425 -0
- package/dist/cjs/types.d.ts +26 -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/fetch.js +2 -1
- 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 +7 -7
- package/dist/esm/commands/list.js +17 -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 +425 -0
- package/dist/esm/control.js +630 -0
- package/dist/esm/dates.d.ts +9 -1
- package/dist/esm/dates.js +20 -1
- package/dist/esm/envelope.d.ts +1 -1
- package/dist/esm/envelope.js +26 -1
- package/dist/esm/index.d.ts +9 -1
- package/dist/esm/index.js +5 -2
- package/dist/esm/load-plugins.d.ts +3 -1
- package/dist/esm/load-plugins.js +28 -4
- package/dist/esm/plugins/acl.js +109 -42
- package/dist/esm/plugins/binary.js +2 -1
- package/dist/esm/plugins/condstore.js +7 -0
- package/dist/esm/plugins/context-search.js +2 -2
- package/dist/esm/plugins/create-special-use.d.ts +0 -5
- package/dist/esm/plugins/create-special-use.js +8 -1
- package/dist/esm/plugins/imap4rev2.js +5 -0
- package/dist/esm/plugins/index.js +1 -3
- package/dist/esm/plugins/metadata-server.d.ts +6 -2
- package/dist/esm/plugins/metadata-server.js +3 -1
- package/dist/esm/plugins/metadata.d.ts +5 -1
- package/dist/esm/plugins/metadata.js +143 -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 +121 -0
- package/dist/esm/random.d.ts +10 -0
- package/dist/esm/random.js +19 -0
- package/dist/esm/rest.d.ts +54 -0
- package/dist/esm/rest.js +559 -0
- package/dist/esm/script.d.ts +33 -4
- package/dist/esm/script.js +63 -4
- package/dist/esm/server.d.ts +199 -9
- package/dist/esm/server.js +606 -230
- 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 +218 -0
- package/dist/esm/store-operations.d.ts +171 -0
- package/dist/esm/store-operations.js +410 -0
- package/dist/esm/types.d.ts +26 -1
- package/dist/plugin-help.json +3 -16
- 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
|
|
|
@@ -71,7 +71,7 @@ ImapKit is extendable: any command can be overridden and plugins can be added (s
|
|
|
71
71
|
|
|
72
72
|
ImapKit is meant for developing standards compliant IMAP clients, so it follows the RFCs strictly instead of accepting whatever clients send. Most production servers are lenient, which hides client bugs until the client meets a stricter server. ImapKit answers these with `BAD` (or `NO` where the RFC requires it):
|
|
73
73
|
|
|
74
|
-
- commands sent in the wrong state (RFC 3501 section 3), for example `FETCH` before `SELECT` or `LOGIN` after login
|
|
74
|
+
- commands sent in the wrong state (RFC 3501 section 3), for example `FETCH` before `SELECT` or `LOGIN` and `AUTHENTICATE` (with any mechanism, known or not) after login
|
|
75
75
|
- arguments to commands that take none (`NOOP x`, `CLOSE x`), missing or extra arguments, and values that break the RFC 3501 grammar
|
|
76
76
|
- command lines that end with a bare LF instead of CRLF
|
|
77
77
|
- literal data sent before the server's `+` continuation request (RFC 3501 section 4.3); `{n+}` is only accepted when LITERAL+ or LITERAL- is enabled, and with LITERAL- only up to 4096 octets, a larger one is answered with `BAD [TOOBIG]` (RFC 7888 section 5)
|
|
@@ -89,6 +89,7 @@ ImapKit is meant for developing standards compliant IMAP clients, so it follows
|
|
|
89
89
|
- the QRESYNC `SELECT` parameter or the `VANISHED` modifier without `ENABLE QRESYNC`, `VANISHED` with `FETCH` or without `CHANGEDSINCE`, and QRESYNC values that break the RFC 7162 grammar (UIDVALIDITY or mod-sequence `0`, `*` in the UID sets, sequence match sets that are not ascending or not of the same size)
|
|
90
90
|
- unknown `SEARCH RETURN` options or `RETURN` after `CHARSET` (RFC 4466 section 2.6.1), `$` combined with numbers, and `SEARCH MODSEQ` values or entry names that break the RFC 7162 grammar
|
|
91
91
|
- extended LIST commands (RFC 5258) with unknown options, `RECURSIVEMATCH` without a base option like `SUBSCRIBED` (also `(SPECIAL-USE RECURSIVEMATCH)`, RFC 6154 section 6), an empty pattern list, options with values they do not take, a repeated `STATUS` return option with different items, and invalid `STATUS` items (RFC 5819)
|
|
92
|
+
- `CREATE ... (USE (...))` (CREATE-SPECIAL-USE) with entries that are not an atom starting with a backslash, like `NIL`, quoted strings or `Sent` (RFC 6154 section 6: `use-attr-ext = "\" atom`); an attribute the server does not support gets `NO [USEATTR]` (section 3)
|
|
92
93
|
- METADATA entry names that break RFC 5464 section 3.2 (`//`, a trailing `/`, `*`, `%`, 8-bit or control characters, a scope other than `/private` or `/shared`), values that are atoms or use bare CR or LF as line ends, empty entry or option lists, and GETMETADATA options after the mailbox name (errata 2785)
|
|
93
94
|
- unknown or uppercase ACL rights, and empty identifiers or identifiers with control characters or invalid UTF-8 (RFC 4314 section 3)
|
|
94
95
|
- more than one message in `APPEND` without MULTIAPPEND, and with MULTIAPPEND a zero-length message literal cancels the whole `APPEND` with `NO` (RFC 3502)
|
|
@@ -101,7 +102,7 @@ ImapKit is meant for developing standards compliant IMAP clients, so it follows
|
|
|
101
102
|
- with NOTIFY (RFC 5465): MessageNew without MessageExpunge or the other way round, FlagChange without both (section 5), mailbox events or two selected filters with `selected`/`selected-delayed` (section 6.1), fetch attributes outside the selected filters, empty event or mailbox lists, `NOTIFY SET` without event groups; unknown events get `NO [BADEVENT (...)]` listing the supported ones (section 3.1)
|
|
102
103
|
- after `ENABLE IMAP4rev2` (RFC 9051): `CHECK`, `LSUB`, the `RFC822`, `RFC822.HEADER` and `RFC822.TEXT` FETCH items, the `NEW`, `OLD` and `RECENT` SEARCH keys and the `RECENT` STATUS item, none of which are in the RFC 9051 grammar (Appendix E), numbers above 63 bits in LARGER and SMALLER, and invalid UTF-8 or mailbox names that are not Net-Unicode (section 5.1); before it, 8-bit quoted strings (Appendix A) and partial ranges, LARGER and SMALLER values above 32 bits (RFC 3501 section 9 number)
|
|
103
104
|
|
|
104
|
-
Responses follow the grammar strictly too: strings that can not be quoted are sent as literals. Failures carry the RFC 5530 response codes that RFC 9051 section 7.1 lists, in both protocol revisions: `AUTHENTICATIONFAILED` and `AUTHORIZATIONFAILED` for logins, `ALREADYEXISTS`, `NONEXISTENT`, `CANNOT`, `HASCHILDREN` and `NOPERM` for mailbox operations, `TRYCREATE` when the target of APPEND, COPY or MOVE does not exist or is a `\Noselect` name, `CLIENTBUG` for `STATUS` on the selected mailbox and for `STORE`, `EXPUNGE`, `UID EXPUNGE`, `MOVE` and `REPLACE` in a mailbox selected read-only, and `EXPUNGEISSUED` when FETCH, STORE, SEARCH, SORT or THREAD completes while the EXPUNGE of another session can not be reported yet.
|
|
105
|
+
Responses follow the grammar strictly too: strings that can not be quoted are sent as literals. Failures carry the RFC 5530 response codes that RFC 9051 section 7.1 lists, in both protocol revisions: `AUTHENTICATIONFAILED` and `AUTHORIZATIONFAILED` for logins, `ALREADYEXISTS`, `NONEXISTENT`, `CANNOT`, `HASCHILDREN` and `NOPERM` for mailbox operations, `TRYCREATE` when the target of APPEND, COPY or MOVE does not exist or is a `\Noselect` name, `CLIENTBUG` for `STATUS` on the selected mailbox and for `STORE`, `EXPUNGE`, `UID EXPUNGE`, `MOVE` and `REPLACE` in a mailbox selected read-only, and `EXPUNGEISSUED` when FETCH, STORE, SEARCH, SORT or THREAD (or UID SEARCH, UID SORT or UID THREAD with message numbers in the criteria) completes while the EXPUNGE of another session can not be reported yet.
|
|
105
106
|
|
|
106
107
|
Some client side recommendations of RFC 2683 are checked too: `STATUS` on the selected mailbox gets `CLIENTBUG` (section 3.1.1), mailbox names must be valid modified UTF-7 (section 3.4.2), and EXPUNGE or STORE after EXAMINE, which answered `[READ-ONLY]`, get `NO` (section 3.3.2). Command lines are not limited to the 1000 octets that section 3.2.1.5 suggests for clients, the server accepts up to 1 MiB (the section asks servers for at least 8000 octets) and answers a longer line with `BAD`.
|
|
107
108
|
|
|
@@ -112,14 +113,16 @@ ImapKit follows these of the strategies that RFC 2180 (IMAP4 Multi-Accessed Mail
|
|
|
112
113
|
- a session that has not been told about the EXPUNGE of another session yet keeps its message numbers. FETCH still returns the expunged messages (section 4.1.1) and SEARCH still finds them (section 4.3), both end with `OK [EXPUNGEISSUED]`
|
|
113
114
|
- STORE does not change expunged messages: with `.SILENT` it ends with `OK` (section 4.2.1), otherwise the other messages are stored and get their FETCH responses, and the tagged response is `NO [EXPUNGEISSUED]` (sections 4.2.2 and 4.2.3; with CONDSTORE `NO [MODIFIED ...]` when that applies, RFC 7162 section 3.1.3)
|
|
114
115
|
- COPY and MOVE of a set that includes an expunged message copy nothing and return the pending EXPUNGE responses with `NO [EXPUNGEISSUED]` (section 4.4.1)
|
|
115
|
-
- UID commands report the pending EXPUNGE responses before they run (RFC 3501 section 7.4.1), the UIDs of the expunged messages then no longer exist and are ignored (RFC 3501 section 6.4.8). UID SEARCH with message numbers in its criteria still uses the old numbers
|
|
116
|
+
- UID commands report the pending EXPUNGE responses before they run (RFC 3501 section 7.4.1), the UIDs of the expunged messages then no longer exist and are ignored (RFC 3501 section 6.4.8). UID SEARCH (UID SORT, UID THREAD) with message numbers in its criteria still uses the old numbers and ends with `OK [EXPUNGEISSUED]`
|
|
117
|
+
- a command that refers to messages (FETCH, STORE, SEARCH, COPY, MOVE, SORT, THREAD and the UID commands) first reports the new messages (EXISTS) and flag changes of other sessions, so every message number in its responses is one the client was told about (RFC 3501 section 5.2: "A server MUST send mailbox size updates automatically if a mailbox size change is observed during the processing of a command"). While an EXPUNGE is pending, only what was queued before it goes out, a message that arrived after it is announced after the EXPUNGE, and until then its number is answered with BAD (RFC 3501 section 9, seq-number)
|
|
118
|
+
- the EXISTS responses of new messages are followed by `* n RECENT` with the number of `\Recent` messages of the session (RFC 3501 section 7.3.2), also for its own APPEND, COPY and MOVE into the selected mailbox, but not after `ENABLE IMAP4rev2`
|
|
116
119
|
- DELETE of a mailbox that other sessions have selected disconnects them with `* BYE` (section 3.3)
|
|
117
120
|
- RENAME keeps the messages of the mailbox under the new name, sessions that have it selected keep working, the old name no longer exists (section 3.4)
|
|
118
121
|
- a session that ends without LOGOUT or CLOSE does not expunge anything (RFC 2683 section 3.1.2), and there is no inactivity timeout
|
|
119
122
|
|
|
120
123
|
## Authentication
|
|
121
124
|
|
|
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 `
|
|
125
|
+
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
126
|
|
|
124
127
|
## Status
|
|
125
128
|
|
|
@@ -130,7 +133,8 @@ All RFC 3501 commands are supported. Some choices that the RFCs leave to the ser
|
|
|
130
133
|
- The subscription list holds names, not mailboxes (RFC 3501 section 6.3.6). DELETE does not unsubscribe, so LSUB and `LIST (SUBSCRIBED)` keep listing the name (as `\NonExistent` in extended LIST) until UNSUBSCRIBE, and a mailbox created again under that name is subscribed. RENAME leaves the subscription with the old name (RFC 9051 section 6.3.6). A mailbox from the storage object is subscribed unless it has `"subscribed": false`, a new mailbox is not. SUBSCRIBE refuses names that are not mailboxes, UNSUBSCRIBE accepts any name
|
|
131
134
|
- CREATE `a/b` also creates `a` as a normal mailbox if it does not exist (RFC 3501 section 6.3.3, Dovecot creates a `\Noselect` level instead). An existing `\Noselect` level stays `\Noselect`
|
|
132
135
|
- DELETE of a mailbox with children leaves a `\Noselect` level that keeps nothing but the children, CREATE of that name makes a new mailbox with a new UIDVALIDITY
|
|
133
|
-
- A keyword stays in the FLAGS and PERMANENTFLAGS of a mailbox once a message in it had the keyword, also after that message is expunged (RFC 3501 section 7.2.6, like Dovecot)
|
|
136
|
+
- A keyword stays in the FLAGS and PERMANENTFLAGS of a mailbox once a message in it had the keyword, also after that message is expunged (RFC 3501 section 7.2.6, like Dovecot). In a mailbox with `"allowPermanentFlags": false` STORE accepts exactly the flags PERMANENTFLAGS lists (`permanentFlags` and the flags its messages have or had) and ignores the others, APPEND and COPY leave them out (RFC 3501 section 7.1)
|
|
137
|
+
- An obsolete source route in an address (`<@route.example:a@b.c>`, RFC 5322 section 4.4) goes to the at-domain-list field of the ENVELOPE address (`(NIL "@route.example" "a" "b.c")`), the mailbox name is the local part only (RFC 9051 section 7.5.2), like Dovecot sends it
|
|
134
138
|
|
|
135
139
|
### Supported Plugins
|
|
136
140
|
|
|
@@ -162,13 +166,13 @@ An unknown plugin name throws an error, and a plugin listed more than once is lo
|
|
|
162
166
|
- **LITERALPLUS** Enables LITERAL+ [RFC7888] capability. Can not be loaded together with LITERALMINUS, but replaces the LITERAL- that IMAP4rev2 loads
|
|
163
167
|
- **LOGINDISABLED** Disables LOGIN support for unencrypted connections
|
|
164
168
|
- **MESSAGELIMIT** Adds MESSAGELIMIT [RFC9738] capability, advertised as `MESSAGELIMIT=<n>`, where the server option `messageLimit` sets n (default 1000, any positive number is accepted so that small test mailboxes can hit it). FETCH, STORE, SEARCH, MOVE, UID EXPUNGE and their UID variants only work on the n messages with the highest UIDs (UID EXPUNGE counts the `\Deleted` ones) and add `[MESSAGELIMIT n uid]` with the lowest processed UID to the tagged OK, or send it in an untagged `NO` when the tagged OK already has a response code (like `HIGHESTMODSEQ` or `MODIFIED`). SEARCH counts the searched messages, which its top level sequence set, `UID`, `UIDAFTER` and `UIDBEFORE` keys narrow down. COPY, APPEND (MULTIAPPEND), SORT and THREAD of more messages, and a FETCH `PARTIAL` range (PARTIAL plugin) of more messages, fail with `NO [MESSAGELIMIT ...]`. EXPUNGE, CLOSE and STATUS are not limited. Adds the `UIDAFTER` and `UIDBEFORE` search keys. Can not be loaded together with SAVELIMIT
|
|
165
|
-
- **METADATA** Adds METADATA [RFC5464] capability (GETMETADATA and SETMETADATA) for server and mailbox annotations. Values can be binary: SETMETADATA takes a literal8 (`~{n}`), and values with NUL are sent back as a literal8. Initial mailbox entries come from a `metadata` object on the mailbox in storage (`"INBOX": { "metadata": { "/private/comment": "My comment" } }`), server entries from the `metadata` option. Server options `metadataMaxSize` (largest value in octets, default 65536), `metadataMaxEntries` (entries per mailbox and for the server, default 100) and `metadataPrivate: false` (refuse `/private` entries with `[METADATA NOPRIVATE]`) let you test the client's error handling. `/shared/admin` on the server is read-only. Annotations move with RENAME (renaming INBOX copies them), DELETE removes them. After `ENABLE METADATA` (needs the ENABLE plugin), changes made by other sessions are announced with unsolicited `METADATA` responses. With SPECIAL-USE loaded, the read-only `/private/specialuse` entry shows the special-use attributes of a mailbox (RFC 6154 section 4)
|
|
169
|
+
- **METADATA** Adds METADATA [RFC5464] capability (GETMETADATA and SETMETADATA) for server and mailbox annotations, and loads ENABLE, which RFC 5464 section 4.1 requires for unsolicited METADATA responses. Values can be binary: SETMETADATA takes a literal8 (`~{n}`), and values with NUL are sent back as a literal8. Initial mailbox entries come from a `metadata` object on the mailbox in storage (`"INBOX": { "metadata": { "/private/comment": "My comment" } }`), server entries from the `metadata` option. Server options `metadataMaxSize` (largest value in octets, default 65536), `metadataMaxEntries` (entries per mailbox and for the server, default 100) and `metadataPrivate: false` (refuse `/private` entries with `[METADATA NOPRIVATE]`) let you test the client's error handling. `/shared/admin` on the server is read-only. Annotations move with RENAME (renaming INBOX copies them), DELETE removes them. After `ENABLE METADATA` (needs the ENABLE plugin), changes made by other sessions are announced with unsolicited `METADATA` responses. With SPECIAL-USE loaded, the read-only `/private/specialuse` entry shows the special-use attributes of a mailbox (RFC 6154 section 4)
|
|
166
170
|
- **MULTISEARCH** Adds MULTISEARCH [RFC7377] capability, also loads ESEARCH: the ESEARCH command, also in the authenticated state. `ESEARCH IN (mailboxes "a" subtree "b" subtree-one "c" personal subscribed inboxes selected) RETURN (...) criteria` sends one ESEARCH response with UIDs and the `TAG`, `MAILBOX` and `UIDVALIDITY` correlators for every mailbox with matches. Mailboxes that do not exist or are `\Noselect` are skipped (with ACL also those without the `r` right, and without `l` unless named under `mailboxes` or as a subtree root), a mailbox named twice is searched once, and `inboxes` is INBOX. `SAVE` is only allowed when the selected mailbox is the only one searched, `UPDATE` (with CONTEXT=SEARCH) only applies to the selected mailbox
|
|
167
|
-
- **METADATA-SERVER** Same as METADATA, but only for server annotations (mailbox name `""`)
|
|
171
|
+
- **METADATA-SERVER** Same as METADATA (also loads ENABLE), but only for server annotations (mailbox name `""`)
|
|
168
172
|
- **MOVE** Adds MOVE [RFC6851] capability (MOVE and UID MOVE commands)
|
|
169
173
|
- **MULTIAPPEND** Adds MULTIAPPEND [RFC3502] capability. APPEND takes several messages and appends all or none of them. With UIDPLUS, APPENDUID lists the UIDs as a UID set
|
|
170
174
|
- **NAMESPACE** Adds NAMESPACE [RFC2342] capability
|
|
171
|
-
- **NOTIFY** Adds NOTIFY [RFC5465] capability: `NOTIFY SET [STATUS] (filter events) ...` and `NOTIFY NONE` with the `selected`, `selected-delayed`, `inboxes` (same as `personal`), `personal`, `subscribed`, `subtree` and `mailboxes` filters and the MessageNew (with fetch attributes for the selected mailbox), MessageExpunge, FlagChange, MailboxName (LIST with `OLDNAME` for RENAME) and SubscriptionChange events, plus MailboxMetadataChange and ServerMetadataChange with METADATA. Events are sent as soon as they happen, also between commands,
|
|
175
|
+
- **NOTIFY** Adds NOTIFY [RFC5465] capability: `NOTIFY SET [STATUS] (filter events) ...` and `NOTIFY NONE` with the `selected`, `selected-delayed`, `inboxes` (same as `personal`), `personal`, `subscribed`, `subtree` and `mailboxes` filters and the MessageNew (with fetch attributes for the selected mailbox), MessageExpunge, FlagChange, MailboxName (LIST with `OLDNAME` for RENAME) and SubscriptionChange events, plus MailboxMetadataChange and ServerMetadataChange with METADATA. Events are sent as soon as they happen, also between commands, while a command runs they wait for its tagged response, and EXPUNGE (or VANISHED) waits longer with `selected-delayed` and during FETCH, STORE and SEARCH. For a new message in the selected mailbox an IMAP4rev1 session gets EXISTS, the requested FETCH and then RECENT. After the first NOTIFY a session only hears about the events it asked for, also for the selected mailbox; changes made by the session itself are not reported. Other mailboxes are reported with STATUS (UNSEEN when the `\Seen` count changed, HIGHESTMODSEQ when CONDSTORE is enabled), with ACL only mailboxes with the `l` and `r` rights, and granting or revoking `l` counts as MailboxName. Fetch attributes never set `\Seen`. `server.notifyOverflow([connection])` sends `* OK [NOTIFICATIONOVERFLOW]` and turns NOTIFY off. AnnotationChange (no ANNOTATE support) is refused with `NO [BADEVENT]`, the fetch attributes of the CONTEXT=SEARCH `UPDATE` option (RFC 5465 section 7) are not supported
|
|
172
176
|
- **OAUTHBEARER** Adds AUTH=OAUTHBEARER [RFC7628] capability, with or without SASL-IR. Uses the same credentials as XOAUTH2: access token `"testtoken"`, the authzid in the GS2 header (`n,a=testuser,`) is optional. A failed login gets the JSON error result as a continuation request (`invalid_token` or `invalid_request`), the client must answer it with `AQ==` (a single `%x01`)
|
|
173
177
|
- **OBJECTID** Adds OBJECTID [RFC8474] capability: `MAILBOXID` for CREATE, SELECT, EXAMINE and STATUS, `EMAILID` and `THREADID` for FETCH and SEARCH. Ids are generated (`F1`, `M1`, `T1`, ...) unless the storage sets a `MAILBOXID` for a mailbox or an `EMAILID` / `THREADID` for a message. COPY, MOVE and RENAME INBOX keep the EMAILID and THREADID of a message. Messages are threaded by their `Message-ID`, `In-Reply-To` and `References` headers across all mailboxes, a message joins the thread of the nearest known parent when it is added
|
|
174
178
|
- **PARTIAL** Adds PARTIAL [RFC9394] capability, also loads ESEARCH: the `PARTIAL` result option of SEARCH (`RETURN (PARTIAL 1:100)`, `RETURN (PARTIAL -1:-100)` counts from the last result) and of SORT with ESORT, and the `PARTIAL` modifier of FETCH and UID FETCH (`UID FETCH 1:* (FLAGS) (PARTIAL -1:-50)`), which combines with CHANGEDSINCE. With PARTIAL loaded a command takes only one PARTIAL or ALL result option
|
|
@@ -178,7 +182,7 @@ An unknown plugin name throws an error, and a plugin listed more than once is lo
|
|
|
178
182
|
- **SASL-IR** Enables SASL-IR [RFC4959] capability
|
|
179
183
|
- **QRESYNC** Adds QRESYNC [RFC7162] capability, also loads CONDSTORE and ENABLE. After `ENABLE QRESYNC`: `SELECT`/`EXAMINE` with `(QRESYNC (uidvalidity modseq [known-uids] [seq-match-data]))` reports `VANISHED (EARLIER)` and the changed flags, `UID FETCH ... (CHANGEDSINCE n VANISHED)` works, and expunges (EXPUNGE, UID EXPUNGE, MOVE, other sessions, IDLE) are reported with `VANISHED` instead of `EXPUNGE`. Expunged UIDs are remembered with their mod-sequence; UIDs missing from the initial storage count as expunged before the server started
|
|
180
184
|
- **SAVELIMIT** Adds SAVELIMIT [RFC9738] capability, advertised as `SAVELIMIT=<n>` (server option `messageLimit`, default 1000): only COPY and APPEND (MULTIAPPEND) of more messages fail with `NO [MESSAGELIMIT ...]`. Can not be loaded together with MESSAGELIMIT
|
|
181
|
-
- **SAVEDATE** Adds SAVEDATE [RFC8514] capability: the `SAVEDATE` FETCH item and the `SAVEDBEFORE`, `SAVEDON`, `SAVEDSINCE` and `SAVEDATESUPPORTED` SEARCH keys. APPEND, COPY and MOVE set the save date to the current time, messages in storage can set it with a `SAVEDATE` value (a date-time string or a Date) and get the time the
|
|
185
|
+
- **SAVEDATE** Adds SAVEDATE [RFC8514] capability: the `SAVEDATE` FETCH item and the `SAVEDBEFORE`, `SAVEDON`, `SAVEDSINCE` and `SAVEDATESUPPORTED` SEARCH keys. APPEND, COPY and MOVE set the save date to the current time, messages in storage can set it with a `SAVEDATE` value (a date-time string or a Date) and get the the time the storage was loaded otherwise (the current time, or the `now` option). A mailbox with `"SAVEDATE": false` in storage does not support save dates: FETCH returns NIL and the SEARCH keys use the internal date
|
|
182
186
|
- **SEARCHRES** Adds SEARCHRES [RFC5182] capability, also loads ESEARCH: `SEARCH RETURN (SAVE)` stores the result and `$` refers to it in FETCH, STORE, COPY, MOVE, UID EXPUNGE, SEARCH and their UID variants. `$` must be used alone, not combined with numbers like `1,$`
|
|
183
187
|
- **SORT** Adds SORT [RFC5256] capability (SORT and UID SORT with all RFC 5256 sort keys). Strings are compared with the i;unicode-casemap collation (RFC 5051), base subjects follow RFC 5256 section 2.1 and sent dates section 2.2. With CONDSTORE, a MODSEQ search key appends the highest mod-sequence (RFC 7162 section 3.1.9). I18NLEVEL=1 is not advertised, as SEARCH matches strings with ASCII case folding only
|
|
184
188
|
- **SORT=DISPLAY** Adds SORT=DISPLAY [RFC5957] capability (DISPLAYFROM and DISPLAYTO sort keys), also loads SORT
|
|
@@ -194,7 +198,6 @@ An unknown plugin name throws an error, and a plugin listed more than once is lo
|
|
|
194
198
|
- **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
199
|
- **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
200
|
- **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
201
|
|
|
199
202
|
## ACL
|
|
200
203
|
|
|
@@ -222,43 +225,23 @@ The rights of other users are enforced as RFC 4314 section 4 describes:
|
|
|
222
225
|
|
|
223
226
|
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
227
|
|
|
225
|
-
##
|
|
228
|
+
## Migrating from 4.x
|
|
226
229
|
|
|
227
|
-
|
|
230
|
+
ImapKit 5.0.0 has two breaking changes:
|
|
228
231
|
|
|
229
|
-
|
|
232
|
+
- SMTP (`--smtpPort`, the `smtp` option) needs the `smtp-server` package, it is an optional peer dependency now: `npm install smtp-server`.
|
|
233
|
+
- The XTOYBIRD plugin was removed, the IMAP port carries IMAP traffic only. Loading `XTOYBIRD` throws an error that points here.
|
|
230
234
|
|
|
231
|
-
|
|
235
|
+
The XTOYBIRD commands map to the [control API](#control-api):
|
|
232
236
|
|
|
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":{...}})`)
|
|
237
|
+
| XTOYBIRD command | Control API |
|
|
238
|
+
| ------------------------------------ | -------------------------------------------------------------------------------------------- |
|
|
239
|
+
| `XTOYBIRD STORAGE` | `server.control.snapshot()` |
|
|
240
|
+
| `XTOYBIRD SERVER` | `server.control.listMailboxes()`, `server.control.listUsers()` |
|
|
241
|
+
| `XTOYBIRD CONNECTION` | `server.control.sessions()` |
|
|
242
|
+
| `XTOYBIRD USERADD "user" "password"` | `server.control.addUser('user', { password })`, or `updateUser()` for an existing user |
|
|
243
|
+
| `XTOYBIRD USERDEL "user"` | `server.control.deleteUser('user')` |
|
|
244
|
+
| `XTOYBIRD SHUTDOWN` | `await server.control.shutdown()`, stops accepting connections and waits for the last client |
|
|
262
245
|
|
|
263
246
|
## CONDSTORE support
|
|
264
247
|
|
|
@@ -276,9 +259,8 @@ S: A1 OK XTOYBIRD Completed
|
|
|
276
259
|
|
|
277
260
|
# Known issues
|
|
278
261
|
|
|
279
|
-
- **addr-adl** (at-domain-list) values are not supported, NIL is always used
|
|
280
262
|
- **anonymous namespaces** are not supported
|
|
281
|
-
- **LIST** does not insert a hierarchy delimiter between a reference without one and the mailbox name (RFC 2683 section 3.4.9 recommends it), the two are concatenated as RFC 9051 section 6.3.9 describes, like Dovecot does
|
|
263
|
+
- **LIST** does not insert a hierarchy delimiter between a reference without one and the mailbox name (RFC 2683 section 3.4.9 recommends it), the two are concatenated as RFC 9051 section 6.3.9 describes, like Dovecot does. A pattern that starts with the `#` break out character ignores the reference
|
|
282
264
|
- **CHARSET** values other than US-ASCII and UTF-8 are not supported
|
|
283
265
|
|
|
284
266
|
# Running tests
|
|
@@ -395,6 +377,118 @@ describe('IMAP tests', () => {
|
|
|
395
377
|
});
|
|
396
378
|
```
|
|
397
379
|
|
|
380
|
+
## Control API
|
|
381
|
+
|
|
382
|
+
`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` (and `RECENT` in IMAP4rev1) 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.
|
|
383
|
+
|
|
384
|
+
```javascript
|
|
385
|
+
const server = imapkit({ plugins: ['IDLE', 'CONDSTORE'] });
|
|
386
|
+
const port = await server.start(); // a free port, server.start(1143, '127.0.0.1') for a fixed one
|
|
387
|
+
|
|
388
|
+
const { uid } = server.control.addMessage('INBOX', { raw: 'Subject: hello\r\n\r\nHi!\r\n', flags: ['\\Seen'] });
|
|
389
|
+
server.control.setFlags('INBOX', [uid], ['\\Flagged'], 'add');
|
|
390
|
+
server.control.expungeMessages('INBOX', [uid]);
|
|
391
|
+
|
|
392
|
+
await server.stop();
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
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` ...).
|
|
396
|
+
|
|
397
|
+
| Method | What it does |
|
|
398
|
+
| ---------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
399
|
+
| `snapshot()` | The storage as JSON in the shape of the `storage` option, `imapkit({ storage: server.control.snapshot() })` starts from the same state |
|
|
400
|
+
| `listMailboxes()`, `getMailbox(path)` | Mailboxes with `path`, `delimiter`, `flags`, `selectable`, `subscribed`, `messages`, `unseen`, `uidnext`, `uidvalidity`, `permanentFlags` (and `highestModseq` with CONDSTORE) |
|
|
401
|
+
| `listMessages(path, { uids, raw })`, `getMessage(path, uid)` | Messages with `uid`, `flags`, `internaldate`, `size` (and `modseq` with CONDSTORE), the source as a Buffer in `raw` |
|
|
402
|
+
| `sessions()` | Connected sessions: `session` number, `user`, `state`, selected `mailbox`, `readOnly`, `enabled`, `secure`, `compressed`, `remoteAddress` |
|
|
403
|
+
| `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`) |
|
|
404
|
+
| `setFlags(path, uids, flags, mode)` | `mode` is `set` (default), `add` or `remove` |
|
|
405
|
+
| `expungeMessages(path, uids)` | Removes messages |
|
|
406
|
+
| `copyMessages(path, uids, target)`, `moveMessages(...)` | Returns `{ uidvalidity, uids: [{ uid, targetUid }] }` |
|
|
407
|
+
| `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 |
|
|
408
|
+
| `createMailbox(path, { subscribed })`, `deleteMailbox(path)`, `renameMailbox(path, newPath)` | Like CREATE, DELETE and RENAME |
|
|
409
|
+
| `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 }] }` |
|
|
410
|
+
| `subscribe(path)`, `unsubscribe(path)` | Return true if the subscription changed |
|
|
411
|
+
| `listUsers()`, `addUser(name, { password, xoauth2 })`, `updateUser(name, {...})`, `deleteUser(name)` | `xoauth2` is `{ accessToken, sessionTimeout }`, `sessionTimeout` is deprecated and ignored (access tokens do not expire). Deleting a user disconnects its sessions, `{ disconnect: false }` keeps them. No credentials in `listUsers()` |
|
|
412
|
+
| `disconnect(session or { user }, { text, reset })` | Disconnects sessions with an untagged `BYE`, or resets the TCP connection |
|
|
413
|
+
| `inject(session, data)` | Writes bytes to a session as they are, e.g. `* OK [ALERT] ...` between commands |
|
|
414
|
+
| `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, with their `hits` counts |
|
|
415
|
+
| `shutdown({ graceful })` | Stops accepting connections and resolves once the last client is gone, `graceful: false` closes the sessions |
|
|
416
|
+
|
|
417
|
+
Plugins add operations for their own data, they exist only when the plugin is loaded:
|
|
418
|
+
|
|
419
|
+
| Plugin | Methods |
|
|
420
|
+
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
421
|
+
| ACL | `getAcl(path)`, `setAcl(path, identifier, rights)` (`+rights` adds, `-rights` removes, empty rights remove the identifier), `deleteAcl(path, identifier)`, ACLs are `{ identifier: rights }` |
|
|
422
|
+
| QUOTA | `getQuota()` (`{ root, limits, usage }`, STORAGE in units of 1024 octets), `setQuota({ STORAGE, MESSAGE, MAILBOX })` replaces the limits |
|
|
423
|
+
| 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 |
|
|
424
|
+
| SPECIAL-USE | `setSpecialUse(path, ['\\Sent'])` (RFC 6154 attributes and `\Important`), `getMailbox()` has `specialUse` |
|
|
425
|
+
| OBJECTID | `getMailbox()` has `mailboxId`, `getMessage()` and `listMessages()` have `emailId` and `threadId` |
|
|
426
|
+
|
|
427
|
+
A plugin of your own adds operations with `server.control.register(name, fn, routes)`, the routes go to the REST API.
|
|
428
|
+
|
|
429
|
+
`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.
|
|
430
|
+
|
|
431
|
+
### Events
|
|
432
|
+
|
|
433
|
+
Tests can wait for events instead of polling:
|
|
434
|
+
|
|
435
|
+
- `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.
|
|
436
|
+
- `command`: `{ session, tag, command, status, user }` when the tagged response of a command goes out, `status` is `OK`, `NO` or `BAD`. A line that is refused before it runs (it does not parse, or it is pipelined ambiguously) and a command that a script rule answers have no `command` event.
|
|
437
|
+
- `mailbox`: `{ type, path, oldPath, mailbox, origin }` for CREATE, DELETE, RENAME, SUBSCRIBE and UNSUBSCRIBE, `origin` is null for the control API.
|
|
438
|
+
- `expunge`: `(mailbox, messages, origin)` before the sessions are told, and `flags`: `(mailbox, messages, origin)` for flag changes of the control API.
|
|
439
|
+
- `acl`: `(mailbox, previousAcl)` when the ACL of a mailbox changes (ACL plugin), `script`: `{ rule, event, session, tag, command }` when a script rule fires, `reset`: after `control.reset()`.
|
|
440
|
+
|
|
441
|
+
```javascript
|
|
442
|
+
const idling = new Promise(resolve => server.on('session', event => event.type === 'waiting' && event.command === 'IDLE' && resolve(event)));
|
|
443
|
+
// ... let the client start IDLE ...
|
|
444
|
+
await idling;
|
|
445
|
+
server.control.addMessage('INBOX', { raw: message }); // the idling client gets * n EXISTS right away
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
## REST API
|
|
449
|
+
|
|
450
|
+
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:
|
|
451
|
+
|
|
452
|
+
```bash
|
|
453
|
+
imapkit -p 1143 --rest-port=8143
|
|
454
|
+
curl -X POST http://127.0.0.1:8143/v1/mailboxes/INBOX/messages \
|
|
455
|
+
-H 'Content-Type: application/json' \
|
|
456
|
+
-d '{"raw": "Subject: hello\r\n\r\nHi!\r\n", "flags": ["\\Seen"]}'
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
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.
|
|
460
|
+
|
|
461
|
+
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.
|
|
462
|
+
|
|
463
|
+
`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:
|
|
464
|
+
|
|
465
|
+
```bash
|
|
466
|
+
curl -N 'http://127.0.0.1:8143/v1/events?types=session'
|
|
467
|
+
# event: session
|
|
468
|
+
# data: {"type":"select","session":{"session":1,"user":"testuser","state":"Selected","mailbox":"INBOX",...}}
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
| Endpoint | Control API |
|
|
472
|
+
| ---------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
|
|
473
|
+
| `GET /v1/snapshot` | `snapshot()` |
|
|
474
|
+
| `POST /v1/reset` | `reset()` |
|
|
475
|
+
| `POST /v1/shutdown` (`{ graceful }`) | `shutdown()` |
|
|
476
|
+
| `GET /v1/sessions`, `DELETE /v1/sessions/{n}` (`{ text, reset }`) | `sessions()`, `disconnect()` |
|
|
477
|
+
| `POST /v1/sessions/{n}/inject` (`{ data, encoding }`) | `inject()` |
|
|
478
|
+
| `GET`, `POST /v1/users`, `PUT`, `DELETE /v1/users/{name}` | `listUsers()`, `addUser()`, `updateUser()`, `deleteUser()` |
|
|
479
|
+
| `GET`, `POST /v1/mailboxes`, `GET`, `DELETE /v1/mailboxes/{path}` | `listMailboxes()`, `createMailbox()`, `getMailbox()`, `deleteMailbox()` |
|
|
480
|
+
| `POST /v1/mailboxes/{path}/rename` (`{ newPath }`) | `renameMailbox()` |
|
|
481
|
+
| `PUT`, `DELETE /v1/mailboxes/{path}/subscription` | `subscribe()`, `unsubscribe()` |
|
|
482
|
+
| `POST /v1/mailboxes/{path}/uidvalidity` | `resetUidValidity()` |
|
|
483
|
+
| `GET /v1/mailboxes/{path}/messages` (`?uids=1,2&raw=true`), `POST` (`{ raw, flags }`) | `listMessages()`, `addMessage()` |
|
|
484
|
+
| `GET`, `DELETE /v1/mailboxes/{path}/messages/{uid}` | `getMessage()`, `expungeMessages()` |
|
|
485
|
+
| `POST /v1/mailboxes/{path}/messages/flags`, `.../expunge`, `.../copy`, `.../move` | `setFlags()`, `expungeMessages()`, `copyMessages()`, `moveMessages()` |
|
|
486
|
+
| `GET /v1/mailboxes/{path}/acl`, `PUT`, `DELETE /v1/mailboxes/{path}/acl/{identifier}` (`{ rights }`) | `getAcl()`, `setAcl()`, `deleteAcl()` (ACL) |
|
|
487
|
+
| `GET`, `PUT /v1/quota` | `getQuota()`, `setQuota()` (QUOTA) |
|
|
488
|
+
| `GET`, `PUT /v1/metadata`, `GET`, `PUT /v1/mailboxes/{path}/metadata` | `getMetadata()`, `setMetadata()` (METADATA) |
|
|
489
|
+
| `PUT /v1/mailboxes/{path}/special-use` (`{ specialUse }`) | `setSpecialUse()` (SPECIAL-USE) |
|
|
490
|
+
| `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 |
|
|
491
|
+
|
|
398
492
|
## Scripted faults
|
|
399
493
|
|
|
400
494
|
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 +523,23 @@ Every rule watches one event (`on`):
|
|
|
429
523
|
- `input`: a line read by a command that takes over the input, like `DONE` of IDLE or a SASL response of AUTHENTICATE
|
|
430
524
|
- `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
525
|
- `continuation`: a `+` continuation request (literals, IDLE, AUTHENTICATE)
|
|
526
|
+
- `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
527
|
|
|
433
528
|
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
529
|
|
|
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
|
|
530
|
+
| Matcher | Events | Matches |
|
|
531
|
+
| -------------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
532
|
+
| `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) |
|
|
533
|
+
| `tag` | all but greeting | the command tag, a string or a RegExp |
|
|
534
|
+
| `description` | response, continuation | the description passed to `connection.send()`, or a list of them. Continuation requests are `LITERAL`, `IDLE`, `AUTHENTICATE PLAIN` and `AUTHENTICATE OAUTHBEARER` |
|
|
535
|
+
| `untagged` | response | `true` for untagged responses only, `false` for tagged ones |
|
|
536
|
+
| `session` | all | the number of the connection, or a list of numbers, 1 for the first connection the server accepted |
|
|
537
|
+
| `state`, `user`, `mailbox` | all | the session state (`'Not Authenticated'`, `'Authenticated'`, `'Selected'`), the authenticated user, the path of the selected mailbox |
|
|
538
|
+
| `match` | all | a RegExp, or a string with a regular expression, tested against the command line or the output bytes (a binary string) |
|
|
539
|
+
| `when` | all | a function that gets the event context and returns true to match |
|
|
540
|
+
| `nth` | all | the rule fires from the nth matching event on (default 1) |
|
|
541
|
+
| `times` | all | the rule fires this many times at most, then lets later rules handle the event |
|
|
542
|
+
| `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
543
|
|
|
448
544
|
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
545
|
|
|
@@ -451,19 +547,19 @@ The actions say what happens instead of the usual behavior. Strings are sent as
|
|
|
451
547
|
| --------------------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
452
548
|
| `send` | all | output events: bytes sent instead of the output. command and input: bytes sent instead of processing the line |
|
|
453
549
|
| `run` | command, input | process the line as usual after `send` (to add output before the real response) |
|
|
454
|
-
| `drop` | all
|
|
550
|
+
| `drop` | all but quiet | output events: send nothing. command and input: ignore the line, the client gets no answer |
|
|
455
551
|
| `mutate` | response, continuation | `(response, context)` gets a copy of the response object before it is compiled and changes it, or returns another one. Continuation requests have a response object only when a plugin sends them with `connection.send()` (the error challenges of XOAUTH2 and OAUTHBEARER), other events ignore `mutate`. A tagged response that does not compile then is sent as `NO [SERVERBUG]`, an untagged one is dropped. Use `send` for output that is not valid IMAP |
|
|
456
552
|
| `literals` | response | sends every string of the response that the grammar allows (string, nstring, astring) as a literal. The positions that take only a quoted string stay quoted: the hierarchy delimiter of LIST, LSUB and NAMESPACE, CHILDINFO values, INTERNALDATE and SAVEDATE, and the media types `"TEXT"` and `"MESSAGE" "RFC822"` in a body structure (RFC 9051 section 9, RFC 5258 section 6). Atoms, response codes and text stay as they are. Works together with `mutate`, after it |
|
|
457
553
|
| `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
554
|
| `before`, `after` | output events | bytes sent before or after the output, e.g. an unsolicited response |
|
|
459
|
-
| `delay` | all but input
|
|
460
|
-
| `chunk`, `chunkDelay` | all | write the bytes in pieces of `chunk` octets, `chunkDelay` milliseconds apart (default 10)
|
|
555
|
+
| `delay` | all but input, quiet | 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 |
|
|
556
|
+
| `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
557
|
| `truncate` | all | send only this many octets of the bytes, then close the connection |
|
|
462
558
|
| `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
559
|
|
|
464
560
|
A rule needs at least one action, and for command and input events `chunk` and `truncate` need `send`. A rule with only `delay` (and `run`) delays the line and then processes it as usual. Rules are checked when they are added: an unknown option, an option that does not apply to the event, or an invalid value throws a `TypeError`, so a typo can not turn into a rule that never fires.
|
|
465
561
|
|
|
466
|
-
The event context, which `when`, `mutate` and `send` functions get, has `event`, `connection`, `session`, `state`, `user`, `mailbox`, `tag`, `command`, `data` (the command line or the output bytes, as a binary string), and for responses `description` and `response`.
|
|
562
|
+
The event context, which `when`, `mutate` and `send` functions get, has `event`, `connection`, `session`, `state`, `user`, `mailbox`, `tag`, `command`, `data` (the command line or the output bytes, as a binary string), `quiet` (milliseconds without input or output, quiet events), and for responses `description` and `response`.
|
|
467
563
|
|
|
468
564
|
`server.script.add(rule)` returns a handle with `rule`, `matched` (events that matched the rule, also before `nth`), `hits` (events the rule handled) and `remove()`, `server.script.add([rules])` returns a list of handles. `server.script.rules` lists the handles in order, `server.script.clear()` removes every rule. The server emits a `script` event `{ rule, event, session, tag, command }` every time a rule fires.
|
|
469
565
|
|
|
@@ -478,6 +574,30 @@ The `imapkit` command takes the rules as JSON with `--script=<path>` (or `IMAPKI
|
|
|
478
574
|
|
|
479
575
|
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
576
|
|
|
577
|
+
### Quirk presets
|
|
578
|
+
|
|
579
|
+
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.
|
|
580
|
+
|
|
581
|
+
| Quirk | Behavior |
|
|
582
|
+
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
583
|
+
| `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) |
|
|
584
|
+
| `james-late-fetch` | Apache James: 1 in 4 FETCH responses come after the tagged OK of their command |
|
|
585
|
+
| `yahoo-quoted-sections` | Yahoo: short body sections (up to 100 octets without line breaks) are quoted strings instead of literals |
|
|
586
|
+
| `m365-throttle` | Microsoft 365: 1 in 10 commands (not LOGOUT) is refused with `BAD Request is throttled. Suggested Backoff Time: 1000 milliseconds` |
|
|
587
|
+
| `no-uidplus`, `no-move` | servers without UIDPLUS or MOVE, the plugins are not loaded even when `plugins` lists them, and with IMAP4rev2 (which requires both, RFC 9051 Appendix E) the server constructor throws |
|
|
588
|
+
|
|
589
|
+
```javascript
|
|
590
|
+
const server = imapkit({ plugins: ['IDLE', 'MOVE'], quirks: ['james-fetchgroup', 'm365-throttle'], scriptSeed: 42 });
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
## Repeatable tests
|
|
594
|
+
|
|
595
|
+
- `scriptSeed`: the seed of the random numbers of script rules with `chance` and the quirk presets that use them (`--script-seed`).
|
|
596
|
+
- `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.
|
|
597
|
+
- `resetUidValidity(path, { uids: 'shuffle', seed })` of the control API.
|
|
598
|
+
|
|
599
|
+
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"?`. A message `internaldate` (or `SAVEDATE`) must be an RFC 3501 date-time string like `"14-Sep-2013 21:22:28 -0300"` or a valid Date, a Date header value like `"Thu, 1 Jan 2026 10:00:00 +0000"` is refused. 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.
|
|
600
|
+
|
|
481
601
|
## Creating custom plugins
|
|
482
602
|
|
|
483
603
|
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.
|
|
@@ -577,7 +697,7 @@ Where
|
|
|
577
697
|
- **mailboxArguments** lists the positions of arguments that are mailbox names, these must be valid modified UTF-7 (RFC 3501 section 5.1.3)
|
|
578
698
|
- **astringArguments** lists the positions of other astring arguments (user names, identifiers). In these, in mailbox names and in search criteria an atom `NIL` reaches the handler as an atom, not as `null`
|
|
579
699
|
- **searchCriteria** is the position where SEARCH style criteria start, **sequenceSet** the position of an argument with message sequence numbers. Both are used for the RFC 3501 section 5.5 pipelining check
|
|
580
|
-
- **noExpunge** if true, EXPUNGE responses are held back while the command runs (like FETCH, STORE and SEARCH, RFC 3501 section 7.4.1)
|
|
700
|
+
- **noExpunge** if true, EXPUNGE responses are held back while the command runs (like FETCH, STORE and SEARCH, RFC 3501 section 7.4.1), the notifications queued before them are still sent
|
|
581
701
|
- **literal8** if true (or the name of the capability that allows it), the command accepts `~{n}` literals (RFC 3516)
|
|
582
702
|
- **noPipelining** if true, the command is refused with BAD when the client sent more input after it (STARTTLS, COMPRESS), and so are the commands sent with it
|
|
583
703
|
- **appendMessage** if true, the command takes a message after its mailbox argument like APPEND (REPLACE), so a message literal to a missing mailbox is refused with `NO [TRYCREATE]` before it is sent
|
|
@@ -589,7 +709,7 @@ Where
|
|
|
589
709
|
server.getCommandOptions(name) -> Object
|
|
590
710
|
server.getCommandStates(name) -> Array|false
|
|
591
711
|
|
|
592
|
-
`getCommandOptions` returns the options of a command (see above) with every key set, `getCommandStates` only the connection states it is valid in
|
|
712
|
+
`getCommandOptions` returns the options of a command (see above) with every key set, `getCommandStates` only the connection states it is valid in (`false` for a command of a plugin that did not limit them).
|
|
593
713
|
|
|
594
714
|
#### Run after every plugin is loaded
|
|
595
715
|
|
|
@@ -691,7 +811,7 @@ server.outputHandlers.push(function (connection, response, description) {
|
|
|
691
811
|
#### Other extension points
|
|
692
812
|
|
|
693
813
|
- `server.messageHandlers` and `server.mailboxHandlers` run on every message and mailbox when it is loaded from storage or created, `(server, message, mailbox)` and `(server, mailbox)`
|
|
694
|
-
- `server.statusHandlers[ITEM]` returns the value of a STATUS item that a plugin adds to `server.allowedStatus`, `(connection, mailbox)`
|
|
814
|
+
- `server.statusHandlers[ITEM]` returns the value of a STATUS item that a plugin adds to `server.allowedStatus`, `(connection, mailbox, status)` where `status` holds the counters of `server.getStatus()`
|
|
695
815
|
- `server.appendChecks` are consulted before APPEND, COPY and MOVE add messages to a mailbox, `(connection, mailbox, messages, options)`. A check returns nothing to allow it, or `{ code, text }` to fail the command with a tagged `NO [code] text` (`{ code, text, soft: true }` only sends an untagged `NO` warning)
|
|
696
816
|
- `server.closedChecks` are consulted when SELECT or EXAMINE closes the selected mailbox, `(connection)`. If any returns true, `* OK [CLOSED]` marks where the responses for the new mailbox start
|
|
697
817
|
- `server.copyHandlers` run when COPY, MOVE or RENAME INBOX copies a message, `(server, source, properties, mailbox)`. Properties set on `properties` are given to the copy before the message handlers run
|
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.
|
|
@@ -58,7 +72,7 @@ Configuration file takes the following structure
|
|
|
58
72
|
}
|
|
59
73
|
},
|
|
60
74
|
"id":{
|
|
61
|
-
"name": "
|
|
75
|
+
"name": "imapkit",
|
|
62
76
|
"version": "__VERSION__",
|
|
63
77
|
"key": "value"
|
|
64
78
|
}
|
|
@@ -68,7 +82,8 @@ Scripted faults
|
|
|
68
82
|
---------------
|
|
69
83
|
|
|
70
84
|
A script file holds a list of rules. Each rule watches one event
|
|
71
|
-
("greeting", "command", "input", "response"
|
|
85
|
+
("greeting", "command", "input", "response", "continuation" or
|
|
86
|
+
"quiet", no input and output for "quietFor" milliseconds) and
|
|
72
87
|
changes what the server does when its matchers fit. For example, this
|
|
73
88
|
script answers the first FETCH with a literal that is too short and
|
|
74
89
|
then drops the connection:
|