imapkit 0.0.0-stage → 4.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/LICENSE +16 -0
- package/README.md +608 -2
- package/bin/help.txt +98 -0
- package/bin/imapkit.js +108 -0
- package/cert/server.crt +20 -0
- package/cert/server.key +28 -0
- package/lib/addressparser.js +283 -0
- package/lib/arguments.js +112 -0
- package/lib/bodystructure.js +149 -0
- package/lib/command-states.js +109 -0
- package/lib/commands/append.js +313 -0
- package/lib/commands/capability.js +47 -0
- package/lib/commands/check.js +21 -0
- package/lib/commands/close.js +30 -0
- package/lib/commands/copy.js +115 -0
- package/lib/commands/create.js +52 -0
- package/lib/commands/delete.js +64 -0
- package/lib/commands/examine.js +7 -0
- package/lib/commands/expunge.js +27 -0
- package/lib/commands/fetch.js +229 -0
- package/lib/commands/handlers/fetch.js +209 -0
- package/lib/commands/handlers/flags.js +42 -0
- package/lib/commands/handlers/search.js +519 -0
- package/lib/commands/handlers/status.js +85 -0
- package/lib/commands/handlers/store.js +127 -0
- package/lib/commands/list.js +100 -0
- package/lib/commands/login.js +67 -0
- package/lib/commands/logout.js +41 -0
- package/lib/commands/lsub.js +87 -0
- package/lib/commands/noop.js +21 -0
- package/lib/commands/rename.js +102 -0
- package/lib/commands/search.js +76 -0
- package/lib/commands/select.js +289 -0
- package/lib/commands/status.js +63 -0
- package/lib/commands/store.js +151 -0
- package/lib/commands/subscribe.js +53 -0
- package/lib/commands/uid copy.js +7 -0
- package/lib/commands/uid fetch.js +5 -0
- package/lib/commands/uid search.js +5 -0
- package/lib/commands/uid store.js +5 -0
- package/lib/commands/unsubscribe.js +50 -0
- package/lib/dates.js +123 -0
- package/lib/deflate-layer.js +232 -0
- package/lib/envelope.js +82 -0
- package/lib/esearch.js +208 -0
- package/lib/framing.js +102 -0
- package/lib/list-extensions.js +36 -0
- package/lib/load-plugins.js +109 -0
- package/lib/mailbox-name.js +133 -0
- package/lib/mimeparser.js +778 -0
- package/lib/mock-client.js +233 -0
- package/lib/numbers.js +52 -0
- package/lib/plugins/acl.js +964 -0
- package/lib/plugins/appendlimit.js +83 -0
- package/lib/plugins/auth-plain.js +94 -0
- package/lib/plugins/binary.js +256 -0
- package/lib/plugins/catenate.js +253 -0
- package/lib/plugins/compress.js +76 -0
- package/lib/plugins/condstore.js +563 -0
- package/lib/plugins/context-search.js +321 -0
- package/lib/plugins/context-sort.js +19 -0
- package/lib/plugins/create-special-use.js +108 -0
- package/lib/plugins/enable.js +155 -0
- package/lib/plugins/esearch.js +156 -0
- package/lib/plugins/esort.js +60 -0
- package/lib/plugins/id.js +138 -0
- package/lib/plugins/idle.js +105 -0
- package/lib/plugins/imap4rev2.js +202 -0
- package/lib/plugins/list-extended.js +258 -0
- package/lib/plugins/list-status.js +31 -0
- package/lib/plugins/literalminus.js +20 -0
- package/lib/plugins/literalplus.js +18 -0
- package/lib/plugins/logindisabled.js +50 -0
- package/lib/plugins/messagelimit.js +234 -0
- package/lib/plugins/metadata-server.js +13 -0
- package/lib/plugins/metadata.js +475 -0
- package/lib/plugins/move.js +110 -0
- package/lib/plugins/multiappend.js +26 -0
- package/lib/plugins/multisearch.js +269 -0
- package/lib/plugins/namespace.js +67 -0
- package/lib/plugins/notify.js +654 -0
- package/lib/plugins/oauthbearer.js +217 -0
- package/lib/plugins/objectid.js +243 -0
- package/lib/plugins/partial.js +68 -0
- package/lib/plugins/preview.js +400 -0
- package/lib/plugins/qresync.js +525 -0
- package/lib/plugins/quota.js +285 -0
- package/lib/plugins/replace.js +145 -0
- package/lib/plugins/sasl-ir.js +12 -0
- package/lib/plugins/savedate.js +59 -0
- package/lib/plugins/savelimit.js +18 -0
- package/lib/plugins/searchres.js +82 -0
- package/lib/plugins/sort-display.js +23 -0
- package/lib/plugins/sort.js +132 -0
- package/lib/plugins/special-use.js +95 -0
- package/lib/plugins/starttls.js +57 -0
- package/lib/plugins/status-size.js +19 -0
- package/lib/plugins/thread-orderedsubject.js +16 -0
- package/lib/plugins/thread-references.js +16 -0
- package/lib/plugins/uidonly.js +135 -0
- package/lib/plugins/uidplus.js +124 -0
- package/lib/plugins/unauthenticate.js +28 -0
- package/lib/plugins/unselect.js +36 -0
- package/lib/plugins/utf8-accept.js +68 -0
- package/lib/plugins/x-gm-ext-1.js +456 -0
- package/lib/plugins/xoauth2.js +188 -0
- package/lib/plugins/xtoybird.js +282 -0
- package/lib/server.js +2880 -0
- package/lib/smtp-listener.js +51 -0
- package/lib/sorting.js +373 -0
- package/lib/threading.js +357 -0
- package/lib/utf8-session.js +123 -0
- package/lib/vanished.js +57 -0
- package/package.json +61 -5
package/README.md
CHANGED
|
@@ -1,3 +1,609 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ImapKit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
ImapKit is a scriptable, in-memory IMAP server for testing IMAP clients. It implements IMAP4rev1 ([RFC 3501](https://www.rfc-editor.org/rfc/rfc3501)) and, as an optional plugin, IMAP4rev2 ([RFC 9051](https://www.rfc-editor.org/rfc/rfc9051)), with more than 50 extensions that can be turned on and off per server instance. Nothing is ever written to disk: the mailbox tree comes from a JSON object, so every new server starts from the same known state.
|
|
4
|
+
|
|
5
|
+
ImapKit is strict on purpose: it answers client input that breaks the RFCs with `BAD` or `NO`, so client bugs show up in your test suite instead of in production (see [Strict by design](#strict-by-design)).
|
|
6
|
+
|
|
7
|
+
[](https://github.com/postalsys/imapkit/actions/workflows/test.yml)
|
|
8
|
+
[](https://www.npmjs.com/package/imapkit)
|
|
9
|
+
[](https://github.com/postalsys/imapkit/blob/master/LICENSE)
|
|
10
|
+
|
|
11
|
+
Homepage: [imapkit.com](https://imapkit.com). ImapKit requires Node.js 20 or newer.
|
|
12
|
+
|
|
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
|
+
|
|
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, storage format and XTOYBIRD commands are unchanged.
|
|
16
|
+
|
|
17
|
+
# Usage
|
|
18
|
+
|
|
19
|
+
### Run as a standalone server
|
|
20
|
+
|
|
21
|
+
Install ImapKit globally with npm and run it:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install -g imapkit
|
|
25
|
+
imapkit -p 1143
|
|
26
|
+
```
|
|
27
|
+
|
|
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
|
+
|
|
30
|
+
`imapkit --smtpPort=1025` also starts an SMTP server that appends every message it receives to INBOX.
|
|
31
|
+
|
|
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
|
+
|
|
34
|
+
### Include as a Node.js module
|
|
35
|
+
|
|
36
|
+
Add `imapkit` dependency
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install imapkit
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Create and start an IMAP server
|
|
43
|
+
|
|
44
|
+
```javascript
|
|
45
|
+
const imapkit = require('imapkit');
|
|
46
|
+
const server = imapkit(options);
|
|
47
|
+
server.listen(1143);
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
See [complete.js](https://github.com/postalsys/imapkit/blob/master/examples/complete.js) for an example.
|
|
51
|
+
|
|
52
|
+
## Scope
|
|
53
|
+
|
|
54
|
+
ImapKit is a single user, multiple connection IMAP server. Changes made over IMAP live only in the memory of that server instance, so start a new server for every test.
|
|
55
|
+
|
|
56
|
+
Several clients can connect to the server simultaneously but all the clients share the same user account, even if login credentials are different. The ACL plugin can limit what users other than the owner can do (see [ACL](#acl)).
|
|
57
|
+
|
|
58
|
+
ImapKit is extendable: any command can be overridden and plugins can be added (see [Creating custom plugins](#creating-custom-plugins), and `lib/commands` and `lib/plugins` for the built-in commands and plugins).
|
|
59
|
+
|
|
60
|
+
## Strict by design
|
|
61
|
+
|
|
62
|
+
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):
|
|
63
|
+
|
|
64
|
+
- commands sent in the wrong state (RFC 3501 section 3), for example `FETCH` before `SELECT` or `LOGIN` after login
|
|
65
|
+
- arguments to commands that take none (`NOOP x`, `CLOSE x`), missing or extra arguments, and values that break the RFC 3501 grammar
|
|
66
|
+
- command lines that end with a bare LF instead of CRLF
|
|
67
|
+
- 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)
|
|
68
|
+
- literals for unknown commands, or for commands that can not run in the current state, are refused without a continuation request
|
|
69
|
+
- mailbox names that are not valid modified UTF-7 (RFC 3501 section 5.1.3), including 8-bit names, and CREATE or RENAME to names with an empty hierarchy level (`foo//bar`, `/foo`, `foo//`), answered with `NO [CANNOT]` (RFC 5530 section 3)
|
|
70
|
+
- invalid sequence sets (`0`, `abc`), message sequence numbers greater than the number of messages in FETCH, STORE, COPY and MOVE, also `*` in an empty mailbox (RFC 3501 section 9, seq-number; UID sets and SEARCH keys can point past the end), flags that are not atoms, `\Recent` in STORE or APPEND, invalid dates
|
|
71
|
+
- 8-bit SEARCH strings without `CHARSET UTF-8`, invalid UTF-8, unsupported charsets (`NO [BADCHARSET]`)
|
|
72
|
+
- SORT and THREAD (RFC 5256 section 5) with a charset that is not an atom or a quoted string, an empty sort criteria list, `REVERSE` that is not followed by a sort key (`REVERSE REVERSE DATE`), or a threading algorithm that is not an atom
|
|
73
|
+
- invalid base64 in SASL exchanges, and anything other than `DONE` while IDLE
|
|
74
|
+
- 8-bit user names or passwords in `LOGIN` (RFC 9755 section 5: UTF-8 user names need `AUTHENTICATE`), and invalid UTF-8 in an `AUTHENTICATE PLAIN` message (RFC 4616 section 2). User names are unicode strings everywhere: the keys of `users`, SASL user names and ACL identifiers
|
|
75
|
+
- OAUTHBEARER client responses that break the RFC 7628 or GS2 (RFC 5801) grammar, and anything other than a single `%x01` after an OAUTHBEARER error result
|
|
76
|
+
- `STARTTLS` and `COMPRESS` with commands pipelined after them (RFC 9051 section 6.2.1, RFC 4978 section 3), TLS or compression is then not started and the pipelined commands are refused with `BAD` without running, and `COMPRESS` while compression is active (`BAD [COMPRESSIONACTIVE]`)
|
|
77
|
+
- pipelined commands that RFC 3501 section 5.5 calls ambiguous, for example `CHECK` followed by `FETCH` without waiting for the `CHECK` result
|
|
78
|
+
- `ENABLE` after `SELECT` or `EXAMINE` (RFC 5161 section 3.1), and `ID` lists that break the RFC 2971 limits
|
|
79
|
+
- 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)
|
|
80
|
+
- 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
|
|
81
|
+
- 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)
|
|
82
|
+
- 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)
|
|
83
|
+
- unknown or uppercase ACL rights, and empty identifiers or identifiers with control characters or invalid UTF-8 (RFC 4314 section 3)
|
|
84
|
+
- more than one message in `APPEND` without MULTIAPPEND, and with MULTIAPPEND a zero-length message literal cancels the whole `APPEND` with `NO` (RFC 3502)
|
|
85
|
+
- CATENATE URLs that are not absolute-path references (`/INBOX/;UID=1`), including relative-path references like `;UID=1` that RFC 5092 section 7.2 forbids, and URLs of message parts that do not exist (`NO [BADURL ...]`)
|
|
86
|
+
- with UIDONLY (RFC 9586): every command that takes message sequence numbers, and sequence sets in search criteria, answered with `BAD [UIDREQUIRED]`
|
|
87
|
+
- `UIDAFTER` and `UIDBEFORE` (MESSAGELIMIT, RFC 9738 section 3.2) with anything but a single UID
|
|
88
|
+
- with UTF8=ACCEPT (RFC 9755): invalid UTF-8 in quoted strings, `SEARCH CHARSET` after `ENABLE UTF8=ACCEPT`, mailbox names with control characters (UTF-8, or encoded in modified UTF-7 like `&AA0-`), U+2028, U+2029, a leading BOM, unassigned code points or a name that is not in Unicode Normalization Form C, and `NO` for `APPEND` of a message with an 8-bit header before `ENABLE UTF8=ACCEPT` (section 4)
|
|
89
|
+
- literal8 (`~{n}`) anywhere but in an APPEND or REPLACE message with BINARY, or a SETMETADATA value with METADATA, refused without a continuation request; NUL octets in a normal literal; a literal8 `TEXT` part in CATENATE
|
|
90
|
+
- with BINARY: `BINARY[]`, `BINARY` of multipart or message/rfc822 parts (RFC 9051 section 6.4.5 allows leaf body parts only), `HEADER`, `TEXT` or `MIME` sections, and a partial range on `BINARY.SIZE`
|
|
91
|
+
- 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)
|
|
92
|
+
- 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)
|
|
93
|
+
|
|
94
|
+
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.
|
|
95
|
+
|
|
96
|
+
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`.
|
|
97
|
+
|
|
98
|
+
## Multiple sessions
|
|
99
|
+
|
|
100
|
+
ImapKit follows these of the strategies that RFC 2180 (IMAP4 Multi-Accessed Mailbox Practice) allows, so a client can be tested against one consistent behavior:
|
|
101
|
+
|
|
102
|
+
- 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]`
|
|
103
|
+
- 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)
|
|
104
|
+
- 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)
|
|
105
|
+
- 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
|
|
106
|
+
- DELETE of a mailbox that other sessions have selected disconnects them with `* BYE` (section 3.3)
|
|
107
|
+
- 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)
|
|
108
|
+
- a session that ends without LOGOUT or CLOSE does not expunge anything (RFC 2683 section 3.1.2), and there is no inactivity timeout
|
|
109
|
+
|
|
110
|
+
## Authentication
|
|
111
|
+
|
|
112
|
+
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 `XTOYBIRD USERADD` adds users at runtime. All users share the same mailbox tree.
|
|
113
|
+
|
|
114
|
+
## Status
|
|
115
|
+
|
|
116
|
+
### IMAP4rev1
|
|
117
|
+
|
|
118
|
+
All RFC 3501 commands are supported. Some choices that the RFCs leave to the server:
|
|
119
|
+
|
|
120
|
+
- 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
|
|
121
|
+
- 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`
|
|
122
|
+
- 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
|
|
123
|
+
- 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)
|
|
124
|
+
|
|
125
|
+
### Supported Plugins
|
|
126
|
+
|
|
127
|
+
Plugins can be enabled when starting the server but can not be unloaded or loaded when the server is already running.
|
|
128
|
+
All plugins are self contained and not tied to core. If you do not enable a plugin, no trace of it is left
|
|
129
|
+
to the system. For example, if you do not enable CONDSTORE, messages do not have a MODSEQ value set.
|
|
130
|
+
Plugin names are case insensitive and capability spellings like `LITERAL+` or `AUTH=PLAIN` are accepted too.
|
|
131
|
+
An unknown plugin name throws an error, and a plugin listed more than once is loaded only once.
|
|
132
|
+
|
|
133
|
+
- **ACL** Adds ACL [RFC4314] capability with `RIGHTS=texk` (SETACL, DELETEACL, GETACL, LISTRIGHTS and MYRIGHTS), and LIST-MYRIGHTS [RFC8440] when LIST-EXTENDED is loaded. See [ACL](#acl) below
|
|
134
|
+
- **APPENDLIMIT** Adds APPENDLIMIT [RFC7889] capability. The server option `appendLimit` (octets) sets the limit for every mailbox and is advertised as `APPENDLIMIT=<n>`. A mailbox in the storage can set its own `appendLimit` (a number, or `null` for no limit), then the capability is advertised without a value and clients read the limits with `STATUS (APPENDLIMIT)`. Larger messages in APPEND and REPLACE fail with `NO [TOOBIG]`, synchronizing literals are refused before the client sends them
|
|
135
|
+
- **AUTH-PLAIN** Adds AUTH=PLAIN capability. Supports SASL-IR [RFC4959] as well
|
|
136
|
+
- **BINARY** Adds BINARY [RFC3516] support: `BINARY[<part>]<<partial>>`, `BINARY.PEEK` and `BINARY.SIZE` FETCH items that remove base64 and quoted-printable encodings (`NO [UNKNOWN-CTE]` for other encodings), and APPEND, MULTIAPPEND and REPLACE with literal8 messages (`~{n}`, `~{n+}` with LITERAL+ or LITERAL-). Decoded data is sent as a literal8 only when it contains NUL. Binary parts of an appended message, and parts with NUL octets, are stored base64 encoded, so `BODY[]` stays valid IMAP4rev1
|
|
137
|
+
- **COMPRESS** Adds COMPRESS=DEFLATE [RFC4978] capability. Raw DEFLATE in both directions after the tagged OK, every burst of responses ends with a sync flush
|
|
138
|
+
- **CATENATE** Adds CATENATE [RFC4469] and URL-PARTIAL [RFC5550] capabilities. APPEND (and REPLACE) can build a message from literals and IMAP URLs of messages or message parts on the server. Only absolute-path URLs are accepted, for example `/INBOX;UIDVALIDITY=1/;UID=2/;SECTION=1.MIME/;PARTIAL=0.100`, other URLs and URLs that do not resolve fail with `NO [BADURL ...]`. A message over the literal size limit fails with `NO [TOOBIG]`. Plugins can refuse URLs of a mailbox through `server.urlAccessChecks`
|
|
139
|
+
- **CONDSTORE** Adds CONDSTORE [RFC7162] support, including the `SEARCH MODSEQ` search key and the `CLOSED` response code
|
|
140
|
+
- **CONTEXT=SEARCH** Adds CONTEXT=SEARCH [RFC5267] capability, also loads ESEARCH: the `UPDATE`, `CONTEXT` and `PARTIAL` result options of SEARCH and UID SEARCH, and the CANCELUPDATE command. With `UPDATE` the session gets `ADDTO` and `REMOVEFROM` ESEARCH updates as messages start or stop matching, whether this or another session changed them (REMOVEFROM comes before the EXPUNGE response, ADDTO after EXISTS and FETCH). Updates end with CANCELUPDATE or when the mailbox is closed. Server option `maxSearchContexts` (default 10) limits the updating searches of a session, above it the server answers with `NO [NOUPDATE "tag"]`. `CONTEXT` is accepted as a hint and ignored. Message numbers in the search program are taken as they were when the search ran (RFC 5267 section 4.3)
|
|
141
|
+
- **CONTEXT=SORT** Adds CONTEXT=SORT [RFC5267] capability, also loads ESORT and CONTEXT=SEARCH: `UPDATE`, `CONTEXT` and `PARTIAL` for SORT and UID SORT, the updates carry context positions in sort order
|
|
142
|
+
- **CREATE-SPECIAL-USE** Enables CREATE-SPECIAL-USE [RFC6154] capability. Allowed special flags can be set with server option `"special-use"`
|
|
143
|
+
- **ESEARCH** Adds ESEARCH [RFC4731] capability: `SEARCH RETURN (MIN MAX ALL COUNT)` and `UID SEARCH RETURN (...)` answer with an ESEARCH response. With CONDSTORE the response includes `MODSEQ` for a `MODSEQ` search
|
|
144
|
+
- **ESORT** Adds ESORT [RFC5267] capability, also loads SORT and ESEARCH: `SORT RETURN (MIN MAX ALL COUNT)` and `UID SORT RETURN (...)` answer with an ESEARCH response in sort order. With SEARCHRES, `SAVE` works for SORT as well
|
|
145
|
+
- **ENABLE** Adds ENABLE capability [RFC5161]. Can be loaded in any order with the plugins it enables (eg. CONDSTORE). Capability names are matched case-insensitively, the ENABLED response lists them as the server advertises them (`IMAP4rev2`, `UTF8=ACCEPT`)
|
|
146
|
+
- **ID** Adds ID [RFC2971] capability
|
|
147
|
+
- **IDLE** Adds IDLE [RFC2177] capability
|
|
148
|
+
- **IMAP4rev2** Adds IMAP4rev2 [RFC9051], advertised next to IMAP4rev1 (Appendix A). Loads the extensions that IMAP4rev2 folds in (ENABLE, NAMESPACE, UNSELECT, UIDPLUS, ESEARCH, SEARCHRES, IDLE, SASL-IR, LIST-EXTENDED, LIST-STATUS, MOVE, BINARY, SPECIAL-USE, STATUS=SIZE, AUTH=PLAIN, and LITERAL- unless LITERAL+ is loaded), and keeps the `$Forwarded`, `$MDNSent`, `$Junk`, `$NotJunk` and `$Phishing` keywords also in mailboxes that do not allow new keywords. Every session starts as IMAP4rev1; after `ENABLE IMAP4rev2` it follows RFC 9051: `STATUS DELETED` is allowed, `SEARCH` answers with `ESEARCH`, `SELECT` and `EXAMINE` send an untagged `LIST` response for the mailbox and `* OK [CLOSED]` when they close one, but no `RECENT` response, `[UNSEEN]` code or `\Recent` flag, mailbox names and quoted strings are UTF-8, SEARCH assumes UTF-8 when no `CHARSET` is given, a message with 8-bit headers can be appended, message/global parts are described and numbered like message/rfc822, and the items that RFC 9051 removed are BAD (see [Strict by design](#strict-by-design)). STARTTLS and LOGINDISABLED (RFC 9051 section 6.1.1) are not loaded, as they change how clients log in; load them when needed. Partial FETCH ranges and the LARGER and SMALLER search keys take 63-bit numbers (number64) after ENABLE, 32-bit ones before it (partial ranges up to 2^53 - 1, the largest exact JavaScript number). A server that advertises only IMAP4rev2, UTF-8 in response text, and OLDNAME are not implemented
|
|
149
|
+
- **LIST-EXTENDED** Adds LIST-EXTENDED [RFC5258]: selection options `SUBSCRIBED`, `REMOTE` (there are no remote mailboxes) and `RECURSIVEMATCH`, return options `SUBSCRIBED` and `CHILDREN`, multiple mailbox patterns and the `CHILDINFO` extended data item. `\Noselect` mailboxes are listed as `\NonExistent` in extended LIST responses. With SPECIAL-USE loaded, the `SPECIAL-USE` selection and return options [RFC6154] combine with the other options. The plain RFC 3501 LIST is not changed
|
|
150
|
+
- **LIST-STATUS** Adds LIST-STATUS [RFC5819], the `STATUS` return option of LIST. Loads LIST-EXTENDED as well
|
|
151
|
+
- **LITERALMINUS** Enables LITERAL- [RFC7888] capability: non-synchronizing literals up to 4096 octets. A larger one is read and dropped, and the command is answered with `BAD [TOOBIG]`. Can not be loaded together with LITERALPLUS
|
|
152
|
+
- **LITERALPLUS** Enables LITERAL+ [RFC7888] capability. Can not be loaded together with LITERALMINUS, but replaces the LITERAL- that IMAP4rev2 loads
|
|
153
|
+
- **LOGINDISABLED** Disables LOGIN support for unencrypted connections
|
|
154
|
+
- **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
|
|
155
|
+
- **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)
|
|
156
|
+
- **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
|
|
157
|
+
- **METADATA-SERVER** Same as METADATA, but only for server annotations (mailbox name `""`)
|
|
158
|
+
- **MOVE** Adds MOVE [RFC6851] capability (MOVE and UID MOVE commands)
|
|
159
|
+
- **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
|
|
160
|
+
- **NAMESPACE** Adds NAMESPACE [RFC2342] capability
|
|
161
|
+
- **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, except EXPUNGE (or VANISHED) with `selected-delayed` and during FETCH, STORE and SEARCH. 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
|
|
162
|
+
- **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`)
|
|
163
|
+
- **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
|
|
164
|
+
- **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
|
|
165
|
+
- **PREVIEW** Adds PREVIEW [RFC8970] capability (the PREVIEW FETCH data item with the LAZY modifier). Previews are generated from the first text/plain or text/html part (text/plain preferred in multipart/alternative, attachments, attached messages and encrypted content are skipped): transfer encoding and charset are decoded, HTML markup and quoted text are removed, whitespace is collapsed and the result is cut to 200 characters. A message in storage can set its own `"preview"` string instead. `PREVIEW (LAZY)` returns NIL until the preview of the message has been generated by a FETCH without LAZY, or comes from storage
|
|
166
|
+
- **QUOTA** Adds QUOTA [RFC9208] capability with `GETQUOTA`, `GETQUOTAROOT`, `SETQUOTA` (`QUOTASET`), the `STORAGE`, `MESSAGE` and `MAILBOX` resources and the `DELETED` and `DELETED-STORAGE` STATUS items. INBOX and the personal namespaces share one quota root, other namespaces have none. Configure it with the `quota` server option, eg. `{ "root": "User quota", "STORAGE": 10240, "MESSAGE": 1000, "MAILBOX": 100, "soft": false }` (STORAGE is in units of 1024 octets, a missing resource is not limited). APPEND, COPY and MOVE (from outside the quota root) fail with `NO [OVERQUOTA]` when they would go over a limit, and CREATE or RENAME INBOX when they would go over the MAILBOX limit. With `"soft": true` they succeed with an untagged `NO [OVERQUOTA]` warning instead. `SETQUOTA` changes the limits at runtime
|
|
167
|
+
- **REPLACE** Adds REPLACE [RFC8508] capability (REPLACE and UID REPLACE commands). With UIDPLUS, APPENDUID is sent in an untagged OK before the EXPUNGE. With QUOTA only the net usage counts (RFC 8508 section 3.4)
|
|
168
|
+
- **SASL-IR** Enables SASL-IR [RFC4959] capability
|
|
169
|
+
- **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
|
|
170
|
+
- **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
|
|
171
|
+
- **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 server was started otherwise. A mailbox with `"SAVEDATE": false` in storage does not support save dates: FETCH returns NIL and the SEARCH keys use the internal date
|
|
172
|
+
- **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,$`
|
|
173
|
+
- **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
|
|
174
|
+
- **SORT=DISPLAY** Adds SORT=DISPLAY [RFC5957] capability (DISPLAYFROM and DISPLAYTO sort keys), also loads SORT
|
|
175
|
+
- **SPECIAL-USE** Enables SPECIAL-USE [RFC6154] capability Mailboxes need to have a "special-use" property (String or Array) that will be used as extra flag for LIST and LSUB responses
|
|
176
|
+
- **STARTTLS** Adds STARTTLS command
|
|
177
|
+
- **STATUS=SIZE** Adds STATUS=SIZE [RFC8438], the `SIZE` status item (also with LIST-STATUS). The plugin file is `status-size`
|
|
178
|
+
- **THREAD=ORDEREDSUBJECT** Adds THREAD=ORDEREDSUBJECT [RFC5256] capability (THREAD and UID THREAD)
|
|
179
|
+
- **THREAD=REFERENCES** Adds THREAD=REFERENCES [RFC5256] capability (THREAD and UID THREAD), the full REFERENCES algorithm of RFC 5256 section 3. Load both THREAD plugins to support both algorithms
|
|
180
|
+
- **UIDONLY** Adds UIDONLY [RFC9586] capability and loads ENABLE. After `ENABLE UIDONLY`, FETCH, STORE, SEARCH, COPY, MOVE, SORT, THREAD and REPLACE, message numbers in the criteria of UID SEARCH, UID SORT, UID THREAD and the ESEARCH command (MULTISEARCH), and the QRESYNC message sequence match data are refused with `BAD [UIDREQUIRED]` (a synchronizing literal of such a command is refused before it is sent). Every FETCH response becomes a `* <uid> UIDFETCH (...)` response (the `UID` item is only included when UID FETCH asks for it), expunges are reported with `VANISHED`, and SELECT does not send `[UNSEEN n]`. EXISTS and RECENT are not changed. Load UIDPLUS for UID EXPUNGE and COPYUID
|
|
181
|
+
- **UIDPLUS** Adds UIDPLUS [RFC4315] capability (APPENDUID, COPYUID and UID EXPUNGE)
|
|
182
|
+
- **UNAUTHENTICATE** Adds UNAUTHENTICATE [RFC8437] capability. Returns to the Not Authenticated state and resets the session: the selected mailbox is closed without expunging, ENABLEd extensions and CONDSTORE are turned off, and COMPRESS ends after the tagged OK. TLS stays
|
|
183
|
+
- **UNSELECT** Adds UNSELECT [RFC3691] capability
|
|
184
|
+
- **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
|
|
185
|
+
- **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
|
|
186
|
+
- **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.
|
|
187
|
+
- **XTOYBIRD** Custom plugin to allow programmatic control of the server. XTOYBIRD commands are only allowed after login
|
|
188
|
+
|
|
189
|
+
## ACL
|
|
190
|
+
|
|
191
|
+
All users share the same mailbox tree. With the ACL plugin, the owner (server option `aclOwner`, `"testuser"` by default) has every right on every mailbox, and every other user only gets the rights that the ACL of a mailbox grants to their user name or to `anyone`, minus the negative rights of `-username` and `-anyone` (RFC 4314 section 2). ACLs come from the `acl` property of a mailbox in the storage, or from SETACL:
|
|
192
|
+
|
|
193
|
+
```json
|
|
194
|
+
{
|
|
195
|
+
"INBOX": { "acl": { "otheruser": "lrs", "anyone": "l" } },
|
|
196
|
+
"": { "folders": { "Shared": { "acl": { "otheruser": "lrswikte", "-otheruser": "t" } } } }
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
The rights of other users are enforced as RFC 4314 section 4 describes:
|
|
201
|
+
|
|
202
|
+
- LIST and LSUB leave out mailboxes without `l`. SELECT, EXAMINE and STATUS need `r`, SUBSCRIBE needs `l`
|
|
203
|
+
- a mailbox is opened READ-ONLY without any of `i`, `e`, `s`, `w` and `t`, and PERMANENTFLAGS only lists the flags the user can change
|
|
204
|
+
- STORE changes only the flags the user has rights for (`s` for `\Seen`, `t` for `\Deleted`, `w` for the others) and answers `NO [NOPERM]` if it could change none of them; a FETCH without `s` does not set `\Seen`
|
|
205
|
+
- APPEND and COPY need `i` on the target and keep only the flags the user has rights for. MOVE also needs `t` and `e` on the source (RFC 6851 section 4.2), and so does REPLACE (RFC 8508 section 4.1). APPEND and REPLACE are refused before the message literal is sent, a target the user can not see like a missing one
|
|
206
|
+
- EXPUNGE needs `e`, CLOSE without `e` closes the mailbox without expunging
|
|
207
|
+
- CREATE needs `k` on the nearest existing parent (so other users can not create top level mailboxes), DELETE needs `x`, RENAME needs `x` on the mailbox and `k` on the new parent
|
|
208
|
+
- GETACL, SETACL, DELETEACL and LISTRIGHTS need `a`, MYRIGHTS needs any of `l`, `r`, `i`, `k`, `x`, `a`
|
|
209
|
+
- with LIST-STATUS, mailboxes without `r` get no STATUS response and are listed with `\Noselect` (RFC 5819 section 2)
|
|
210
|
+
- with METADATA, GETMETADATA and SETMETADATA on a mailbox need `l` and any of `r`, `s`, `w`, `i`, `p` (RFC 5464 section 3.3), and unsolicited METADATA responses only go to sessions with these rights
|
|
211
|
+
- with QUOTA, GETQUOTAROOT only lists the MAILBOX resource without `r` on the mailbox, and SETQUOTA needs `a` on every mailbox of the quota root (RFC 9208 section 6)
|
|
212
|
+
|
|
213
|
+
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.
|
|
214
|
+
|
|
215
|
+
## Existing XTOYBIRD commands
|
|
216
|
+
|
|
217
|
+
To use these functions, XTOYBIRD plugin needs to be enabled and the client needs to be logged in.
|
|
218
|
+
|
|
219
|
+
XTOYBIRD is a test control plugin, not an IMAP extension: it skips every access check, so any user that may use it can read the whole storage (all users' mailboxes with `XTOYBIRD STORAGE`), add users and shut the server down. Load it only in tests that need it. With the ACL plugin only the owner (`aclOwner` option, default `testuser`) may use XTOYBIRD, other users get `NO [NOPERM]`.
|
|
220
|
+
|
|
221
|
+
Available commands:
|
|
222
|
+
|
|
223
|
+
- **XTOYBIRD SERVER** dumps server internals
|
|
224
|
+
- **XTOYBIRD CONNECTION** dumps connection internals
|
|
225
|
+
- **XTOYBIRD STORAGE** dumps storage as JSON
|
|
226
|
+
- **XTOYBIRD USERADD "username" "password"** adds or updates user
|
|
227
|
+
- **XTOYBIRD USERDEL "username"** removes a user
|
|
228
|
+
- **XTOYBIRD SHUTDOWN** Closes the server after the last client disconnects. New connections are rejected.
|
|
229
|
+
|
|
230
|
+
Example usage for XTOYBIRD STORAGE:
|
|
231
|
+
|
|
232
|
+
```
|
|
233
|
+
S: * OK ImapKit ready for rumble
|
|
234
|
+
C: A0 LOGIN testuser testpass
|
|
235
|
+
S: A0 OK User logged in
|
|
236
|
+
C: A1 XTOYBIRD STORAGE
|
|
237
|
+
S: * XTOYBIRD [XJSONDUMP] {3224}
|
|
238
|
+
S: {
|
|
239
|
+
S: "INBOX": {
|
|
240
|
+
S: "messages": [
|
|
241
|
+
S: {
|
|
242
|
+
S: "raw": "Subject: hello 1\r\n\r\nWorld 1!",
|
|
243
|
+
S: ...
|
|
244
|
+
S: A1 OK XTOYBIRD Completed
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Ideas for future XTOYBIRD commands
|
|
248
|
+
|
|
249
|
+
- Change UIDVALIDITY at runtime (eg. `A1 XTOYBIRD UIDVALIDITY INBOX 123` where 123 is the new UIDVALIDITY for INBOX)
|
|
250
|
+
- Reset the server to its initial state (`A1 XTOYBIRD RESET`)
|
|
251
|
+
- Replace the storage at runtime with a JSON string that describes the entire storage (`A1 XTOYBIRD UPDATE {123}\r\n{"INBOX":{...}})`)
|
|
252
|
+
|
|
253
|
+
## CONDSTORE support
|
|
254
|
+
|
|
255
|
+
- All messages have MODSEQ value
|
|
256
|
+
- CONDSTORE can be ENABLEd
|
|
257
|
+
- SELECT/EXAMINE show HIGHESTMODSEQ
|
|
258
|
+
- SELECT/EXAMINE support (CONDSTORE) option
|
|
259
|
+
- Updating flags increments MODSEQ value
|
|
260
|
+
- FETCH (MODSEQ) works
|
|
261
|
+
- FETCH (CHANGEDSINCE modseq) works
|
|
262
|
+
- STORE (UNCHANGEDSINCE modseq) works, messages changed since then are reported with `[MODIFIED ...]`
|
|
263
|
+
- SEARCH MODSEQ works, the entry name and type are checked but ignored since MODSEQ is not stored per flag
|
|
264
|
+
- Flag changes made by other sessions include MODSEQ once CONDSTORE is enabled
|
|
265
|
+
- SELECT/EXAMINE send `* OK [CLOSED]` when they close the selected mailbox
|
|
266
|
+
|
|
267
|
+
# Known issues
|
|
268
|
+
|
|
269
|
+
- **addr-adl** (at-domain-list) values are not supported, NIL is always used
|
|
270
|
+
- **anonymous namespaces** are not supported
|
|
271
|
+
- **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
|
|
272
|
+
- **CHARSET** values other than US-ASCII and UTF-8 are not supported
|
|
273
|
+
|
|
274
|
+
# Running tests
|
|
275
|
+
|
|
276
|
+
Tests use the built-in Node.js test runner, linting uses ESLint and formatting uses Prettier.
|
|
277
|
+
|
|
278
|
+
npm install
|
|
279
|
+
npm test # lint + all tests
|
|
280
|
+
npm run test:unit # tests only
|
|
281
|
+
npm run test:coverage # tests with coverage (Node.js 22.8 or newer)
|
|
282
|
+
node --test test/fetch.js # a single test file
|
|
283
|
+
npm run format # apply Prettier formatting
|
|
284
|
+
|
|
285
|
+
`compare/` holds a development tool that replays the same IMAP commands against ImapKit and a Dovecot server running in Docker and shows where the responses differ (`npm run dovecot:start`, then `npm run compare -- compare/scenarios/fetch.txt`). See [CLAUDE.md](CLAUDE.md#comparing-with-dovecot) for details.
|
|
286
|
+
|
|
287
|
+
## Example storage
|
|
288
|
+
|
|
289
|
+
The `storage` option (or `--storage=<path>` for the command) describes the mailbox tree. The keys are namespaces.
|
|
290
|
+
|
|
291
|
+
### Cyrus
|
|
292
|
+
|
|
293
|
+
storage.json:
|
|
294
|
+
|
|
295
|
+
```json
|
|
296
|
+
{
|
|
297
|
+
"INBOX": {},
|
|
298
|
+
"INBOX.": {},
|
|
299
|
+
"user.": {
|
|
300
|
+
"type": "user"
|
|
301
|
+
},
|
|
302
|
+
"": {
|
|
303
|
+
"type": "shared"
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Gmail
|
|
309
|
+
|
|
310
|
+
storage.json:
|
|
311
|
+
|
|
312
|
+
```json
|
|
313
|
+
{
|
|
314
|
+
"INBOX": {},
|
|
315
|
+
"": {
|
|
316
|
+
"separator": "/",
|
|
317
|
+
"folders": {
|
|
318
|
+
"[Gmail]": {
|
|
319
|
+
"flags": ["\\Noselect"],
|
|
320
|
+
"folders": {
|
|
321
|
+
"All Mail": {
|
|
322
|
+
"special-use": "\\All"
|
|
323
|
+
},
|
|
324
|
+
"Drafts": {
|
|
325
|
+
"special-use": "\\Drafts"
|
|
326
|
+
},
|
|
327
|
+
"Important": {
|
|
328
|
+
"special-use": "\\Important"
|
|
329
|
+
},
|
|
330
|
+
"Sent Mail": {
|
|
331
|
+
"special-use": "\\Sent"
|
|
332
|
+
},
|
|
333
|
+
"Spam": {
|
|
334
|
+
"special-use": "\\Junk"
|
|
335
|
+
},
|
|
336
|
+
"Starred": {
|
|
337
|
+
"special-use": "\\Flagged"
|
|
338
|
+
},
|
|
339
|
+
"Trash": {
|
|
340
|
+
"special-use": "\\Trash"
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
## Use ImapKit for testing your client
|
|
350
|
+
|
|
351
|
+
Creating your tests in Node.js is a piece of cake, you do not even need to run the `imapkit` command. Here is a sample test using the built-in [Node.js test runner](https://nodejs.org/api/test.html).
|
|
352
|
+
|
|
353
|
+
```javascript
|
|
354
|
+
const { describe, it, beforeEach, afterEach } = require('node:test');
|
|
355
|
+
const imapkit = require('imapkit');
|
|
356
|
+
const myIMAPClient = require('../my-imap-client');
|
|
357
|
+
|
|
358
|
+
describe('IMAP tests', () => {
|
|
359
|
+
let server;
|
|
360
|
+
|
|
361
|
+
// Executed before every test, creates a new blank IMAP server
|
|
362
|
+
// on a random free port
|
|
363
|
+
beforeEach((t, done) => {
|
|
364
|
+
server = imapkit();
|
|
365
|
+
server.listen(0, done);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// Executed after every test, closes the IMAP server created for the test
|
|
369
|
+
afterEach((t, done) => {
|
|
370
|
+
server.close(done);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// A new IMAP client is instantiated that tries to connect to the
|
|
374
|
+
// IMAP server. If the client is connected the test is considered as passed.
|
|
375
|
+
it('Connect to the server', (t, done) => {
|
|
376
|
+
const client = myIMAPClient.connect('localhost', server.address().port);
|
|
377
|
+
client.on('ready', () => {
|
|
378
|
+
client.disconnect();
|
|
379
|
+
done();
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
## Creating custom plugins
|
|
386
|
+
|
|
387
|
+
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.
|
|
388
|
+
|
|
389
|
+
```javascript
|
|
390
|
+
imapkit({
|
|
391
|
+
// Add two plugins, built in "IDLE" and custom function
|
|
392
|
+
plugins: ['IDLE', myAwesomePlugin]
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// Plugin handler
|
|
396
|
+
function myAwesomePlugin(server) {
|
|
397
|
+
// Add a string to the capability listing
|
|
398
|
+
server.registerCapability('XSUM');
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Add a new command XSUM
|
|
402
|
+
* If client runs this command, the response is a sum of all
|
|
403
|
+
* numeric arguments provided
|
|
404
|
+
*
|
|
405
|
+
* A1 XSUM 1 2 3 4 5
|
|
406
|
+
* * XSUM 15
|
|
407
|
+
* A1 OK SUM completed
|
|
408
|
+
*
|
|
409
|
+
* @param {Object} connection - Session instance
|
|
410
|
+
* @param {Object} parsed - Input from the client in structured form
|
|
411
|
+
* @param {String} data - Input command as a binary string
|
|
412
|
+
* @param {Function} callback - callback function to run
|
|
413
|
+
*/
|
|
414
|
+
server.setCommandHandler('XSUM', function (connection, parsed, data, callback) {
|
|
415
|
+
// Send untagged XSUM response
|
|
416
|
+
connection.send(
|
|
417
|
+
{
|
|
418
|
+
tag: '*',
|
|
419
|
+
command: 'XSUM',
|
|
420
|
+
attributes: [
|
|
421
|
+
[].concat(parsed.attributes || []).reduce(function (prev, cur) {
|
|
422
|
+
return prev + Number(cur.value);
|
|
423
|
+
}, 0)
|
|
424
|
+
]
|
|
425
|
+
},
|
|
426
|
+
'XSUM',
|
|
427
|
+
parsed,
|
|
428
|
+
data
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
// Send tagged OK response
|
|
432
|
+
connection.send(
|
|
433
|
+
{
|
|
434
|
+
tag: parsed.tag,
|
|
435
|
+
command: 'OK',
|
|
436
|
+
attributes: [
|
|
437
|
+
// TEXT allows to send unquoted
|
|
438
|
+
{ type: 'TEXT', value: 'XSUM completed' }
|
|
439
|
+
]
|
|
440
|
+
},
|
|
441
|
+
'XSUM',
|
|
442
|
+
parsed,
|
|
443
|
+
data
|
|
444
|
+
);
|
|
445
|
+
callback();
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
### Plugin methods
|
|
451
|
+
|
|
452
|
+
#### Add a capability
|
|
453
|
+
|
|
454
|
+
server.registerCapability(name[, availability])
|
|
455
|
+
|
|
456
|
+
Where
|
|
457
|
+
|
|
458
|
+
- **name** a string displayed in the capability response
|
|
459
|
+
- **availability** a function which returns boolean value. Executed before displaying the capability response. If the function returns true, the capability is displayed, if false then not.
|
|
460
|
+
|
|
461
|
+
Example
|
|
462
|
+
|
|
463
|
+
```javascript
|
|
464
|
+
// Display in CAPABILITY only in Not Authenticated state
|
|
465
|
+
server.registerCapability('XAUTH', function (connection) {
|
|
466
|
+
return connection.state === 'Not Authenticated';
|
|
467
|
+
});
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
#### Define a command
|
|
471
|
+
|
|
472
|
+
server.setCommandHandler(name, handler[, options])
|
|
473
|
+
|
|
474
|
+
Where
|
|
475
|
+
|
|
476
|
+
- **name** is the command name
|
|
477
|
+
- **handler** _(connection, parsed, data, callback)_ is the handler function for the command
|
|
478
|
+
- **options** is an optional object, checked by the server before the handler runs:
|
|
479
|
+
- **states** lists the connection states the command is valid in (`'Not Authenticated'`, `'Authenticated'`, `'Selected'`), any state if not set. A plain list is read as the states
|
|
480
|
+
- **noArguments** if true, the command is refused when it has arguments
|
|
481
|
+
- **mailboxArguments** lists the positions of arguments that are mailbox names, these must be valid modified UTF-7 (RFC 3501 section 5.1.3)
|
|
482
|
+
- **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`
|
|
483
|
+
- **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
|
|
484
|
+
- **noExpunge** if true, EXPUNGE responses are held back while the command runs (like FETCH, STORE and SEARCH, RFC 3501 section 7.4.1)
|
|
485
|
+
- **literal8** if true (or the name of the capability that allows it), the command accepts `~{n}` literals (RFC 3516)
|
|
486
|
+
- **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
|
|
487
|
+
- **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
|
|
488
|
+
|
|
489
|
+
Without options, a command that already exists (such as a built-in one that the handler wraps) keeps its settings.
|
|
490
|
+
|
|
491
|
+
#### Inspect command options
|
|
492
|
+
|
|
493
|
+
server.getCommandOptions(name) -> Object
|
|
494
|
+
server.getCommandStates(name) -> Array|false
|
|
495
|
+
|
|
496
|
+
`getCommandOptions` returns the options of a command (see above) with every key set, `getCommandStates` only the connection states it is valid in, or `false` if any state is fine.
|
|
497
|
+
|
|
498
|
+
#### Run after every plugin is loaded
|
|
499
|
+
|
|
500
|
+
A plugin that wraps commands or handlers of other plugins, whatever the load order, does it once all plugins are loaded:
|
|
501
|
+
|
|
502
|
+
```javascript
|
|
503
|
+
server.once('pluginsLoaded', function () {
|
|
504
|
+
const move = server.getCommandHandler('MOVE');
|
|
505
|
+
// ...
|
|
506
|
+
});
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
Handler arguments
|
|
510
|
+
|
|
511
|
+
- **connection** - Session instance
|
|
512
|
+
- **parsed** - Input from the client in structured form (see [imap-handler](https://github.com/postalsys/imap-handler#parse-imap-commands) for reference)
|
|
513
|
+
- **data** - Input command as a binary string
|
|
514
|
+
- **callback** - callback function to run (does not take any arguments)
|
|
515
|
+
|
|
516
|
+
The command should send data to the client with `connection.send()`
|
|
517
|
+
|
|
518
|
+
connection.send(response, description, parsed, data, /* any additional data */)
|
|
519
|
+
|
|
520
|
+
Where
|
|
521
|
+
|
|
522
|
+
- **response** is a [imap-handler](https://github.com/postalsys/imap-handler#parse-imap-commands) compatible object. To get the correct tag for the OK, NO or BAD response, look into `parsed.tag`
|
|
523
|
+
- **description** is a string identifying the response to be used by other plugins
|
|
524
|
+
- **parsed** is the `parsed` argument passed to the handler
|
|
525
|
+
- **data** is the `data` argument passed to the handler
|
|
526
|
+
- additional arguments can be used to provide input for other plugins
|
|
527
|
+
|
|
528
|
+
#### Retrieve an existing handler
|
|
529
|
+
|
|
530
|
+
To override existing commands you should first cache the existing command, so you can use it in your own command handler.
|
|
531
|
+
|
|
532
|
+
server.getCommandHandler(name) -> Function
|
|
533
|
+
|
|
534
|
+
Where
|
|
535
|
+
|
|
536
|
+
- **name** is the function name
|
|
537
|
+
|
|
538
|
+
Example
|
|
539
|
+
|
|
540
|
+
```javascript
|
|
541
|
+
const list = server.getCommandHandler('LIST');
|
|
542
|
+
server.setCommandHandler('LIST', function (connection, parsed, data, callback) {
|
|
543
|
+
// do something
|
|
544
|
+
console.log('Received LIST request');
|
|
545
|
+
// run the cached command
|
|
546
|
+
list(connection, parsed, data, callback);
|
|
547
|
+
});
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
#### Reroute input from the client
|
|
551
|
+
|
|
552
|
+
If your plugin needs to get direct input from the client, you can reroute the incoming data by defining a `connection.inputHandler` function. The function gets input data as complete lines (without the linebreaks). Once you want to reroute the input back to the command handler, just clear the function.
|
|
553
|
+
|
|
554
|
+
```
|
|
555
|
+
connection.inputHandler = function(line){
|
|
556
|
+
console.log(line);
|
|
557
|
+
connection.inputHandler = false;
|
|
558
|
+
}
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
See [idle.js](https://github.com/postalsys/imapkit/blob/master/lib/plugins/idle.js) for an example
|
|
562
|
+
|
|
563
|
+
Raw output, such as a `+` continuation request, goes through `connection.write(data)`, and `connection.end()` closes the connection once all output is written. Do not use `connection.socket` for this, a COMPRESS layer (`connection.transport`) sits between the protocol and the socket.
|
|
564
|
+
|
|
565
|
+
#### Reset session state
|
|
566
|
+
|
|
567
|
+
UNAUTHENTICATE (RFC 8437) returns a connection to the Not Authenticated state. A plugin that keeps per-session state on the connection object adds a handler that clears it:
|
|
568
|
+
|
|
569
|
+
```javascript
|
|
570
|
+
server.resetHandlers.push(function (connection) {
|
|
571
|
+
connection.mySessionState = false;
|
|
572
|
+
});
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
#### Override output
|
|
576
|
+
|
|
577
|
+
Any response sent to the client can be overridden or cancelled by other handlers. You should append your handler to `server.outputHandlers` array. If something is being sent to the client, the response object is passed through all handlers in this array.
|
|
578
|
+
|
|
579
|
+
server.outputHandlers.push(function(connection, /* arguments from connection.send */){})
|
|
580
|
+
|
|
581
|
+
`response` arguments from `connection.send` is an object and thus any modifications will be passed on. If `skipResponse` property is added to the response object, the data is not sent to the client.
|
|
582
|
+
|
|
583
|
+
```javascript
|
|
584
|
+
// All untagged responses are ignored and not passed to the client
|
|
585
|
+
server.outputHandlers.push(function (connection, response, description) {
|
|
586
|
+
if (response.tag === '*') {
|
|
587
|
+
response.skipResponse = true;
|
|
588
|
+
console.log('Ignoring untagged response for %s', description);
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
#### Other extension points
|
|
594
|
+
|
|
595
|
+
- `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)`
|
|
596
|
+
- `server.statusHandlers[ITEM]` returns the value of a STATUS item that a plugin adds to `server.allowedStatus`, `(connection, mailbox)`
|
|
597
|
+
- `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)
|
|
598
|
+
- `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
|
|
599
|
+
- `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
|
|
600
|
+
|
|
601
|
+
#### Other possible operations
|
|
602
|
+
|
|
603
|
+
It is possible to append messages to a mailbox; create, delete and rename mailboxes; change authentication state and so on through the `server` and `connection` methods and properties. See existing command handlers and plugins for examples.
|
|
604
|
+
|
|
605
|
+
# License
|
|
606
|
+
|
|
607
|
+
Copyright (c) 2013-2026 Postal Systems OÜ
|
|
608
|
+
|
|
609
|
+
Licensed under the MIT license.
|