imapkit 4.0.3 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -19
- package/bin/imapkit.js +12 -16
- package/dist/cjs/addressparser.d.ts +21 -0
- package/dist/cjs/addressparser.js +277 -0
- package/dist/cjs/arguments.d.ts +31 -0
- package/{lib → dist/cjs}/arguments.js +9 -21
- package/dist/cjs/bodystructure.d.ts +25 -0
- package/dist/cjs/bodystructure.js +150 -0
- package/dist/cjs/cert.d.ts +2 -0
- package/dist/cjs/cert.js +7 -0
- package/dist/cjs/command-states.d.ts +36 -0
- package/dist/cjs/command-states.js +102 -0
- package/dist/cjs/commands/append.d.ts +88 -0
- package/dist/cjs/commands/append.js +299 -0
- package/dist/cjs/commands/capability.d.ts +2 -0
- package/dist/cjs/commands/capability.js +44 -0
- package/dist/cjs/commands/check.d.ts +2 -0
- package/dist/cjs/commands/check.js +28 -0
- package/dist/cjs/commands/close.d.ts +2 -0
- package/dist/cjs/commands/close.js +35 -0
- package/dist/cjs/commands/copy.d.ts +30 -0
- package/dist/cjs/commands/copy.js +107 -0
- package/dist/cjs/commands/create.d.ts +2 -0
- package/dist/cjs/commands/create.js +53 -0
- package/dist/cjs/commands/delete.d.ts +2 -0
- package/dist/cjs/commands/delete.js +63 -0
- package/dist/cjs/commands/examine.d.ts +2 -0
- package/dist/cjs/commands/examine.js +19 -0
- package/dist/cjs/commands/expunge.d.ts +2 -0
- package/dist/cjs/commands/expunge.js +32 -0
- package/dist/cjs/commands/fetch.d.ts +23 -0
- package/dist/cjs/commands/fetch.js +208 -0
- package/dist/cjs/commands/handlers/fetch.d.ts +12 -0
- package/dist/cjs/commands/handlers/fetch.js +195 -0
- package/dist/cjs/commands/handlers/flags.d.ts +17 -0
- package/dist/cjs/commands/handlers/flags.js +39 -0
- package/dist/cjs/commands/handlers/search.d.ts +85 -0
- package/dist/cjs/commands/handlers/search.js +504 -0
- package/dist/cjs/commands/handlers/status.d.ts +34 -0
- package/dist/cjs/commands/handlers/status.js +81 -0
- package/dist/cjs/commands/handlers/store.d.ts +3 -0
- package/dist/cjs/commands/handlers/store.js +117 -0
- package/dist/cjs/commands/index.d.ts +2 -0
- package/dist/cjs/commands/index.js +61 -0
- package/dist/cjs/commands/list.d.ts +2 -0
- package/dist/cjs/commands/list.js +86 -0
- package/dist/cjs/commands/login.d.ts +2 -0
- package/dist/cjs/commands/login.js +62 -0
- package/dist/cjs/commands/logout.d.ts +2 -0
- package/dist/cjs/commands/logout.js +40 -0
- package/dist/cjs/commands/lsub.d.ts +2 -0
- package/dist/cjs/commands/lsub.js +79 -0
- package/dist/cjs/commands/noop.d.ts +2 -0
- package/dist/cjs/commands/noop.js +28 -0
- package/dist/cjs/commands/rename.d.ts +2 -0
- package/dist/cjs/commands/rename.js +98 -0
- package/dist/cjs/commands/search.d.ts +10 -0
- package/dist/cjs/commands/search.js +71 -0
- package/dist/cjs/commands/select.d.ts +19 -0
- package/dist/cjs/commands/select.js +230 -0
- package/dist/cjs/commands/status.d.ts +2 -0
- package/dist/cjs/commands/status.js +57 -0
- package/dist/cjs/commands/store.d.ts +10 -0
- package/dist/cjs/commands/store.js +134 -0
- package/dist/cjs/commands/subscribe.d.ts +2 -0
- package/dist/cjs/commands/subscribe.js +51 -0
- package/dist/cjs/commands/uid-copy.d.ts +2 -0
- package/dist/cjs/commands/uid-copy.js +19 -0
- package/dist/cjs/commands/uid-fetch.d.ts +3 -0
- package/dist/cjs/commands/uid-fetch.js +17 -0
- package/dist/cjs/commands/uid-search.d.ts +3 -0
- package/dist/cjs/commands/uid-search.js +17 -0
- package/dist/cjs/commands/uid-store.d.ts +3 -0
- package/dist/cjs/commands/uid-store.js +17 -0
- package/dist/cjs/commands/unsubscribe.d.ts +2 -0
- package/dist/cjs/commands/unsubscribe.js +50 -0
- package/dist/cjs/dates.d.ts +64 -0
- package/dist/cjs/dates.js +124 -0
- package/dist/cjs/deflate-layer.d.ts +83 -0
- package/dist/cjs/deflate-layer.js +232 -0
- package/dist/cjs/encoded-words.d.ts +15 -0
- package/dist/cjs/encoded-words.js +85 -0
- package/dist/cjs/envelope.d.ts +33 -0
- package/dist/cjs/envelope.js +86 -0
- package/dist/cjs/esearch.d.ts +96 -0
- package/dist/cjs/esearch.js +190 -0
- package/dist/cjs/framing.d.ts +66 -0
- package/{lib → dist/cjs}/framing.js +9 -26
- package/dist/cjs/index.d.ts +9 -0
- package/dist/cjs/index.js +56 -0
- package/dist/cjs/list-extensions.d.ts +50 -0
- package/dist/cjs/list-extensions.js +35 -0
- package/dist/cjs/load-plugins.d.ts +21 -0
- package/dist/cjs/load-plugins.js +89 -0
- package/dist/cjs/mailbox-name.d.ts +26 -0
- package/dist/cjs/mailbox-name.js +127 -0
- package/dist/cjs/mimeparser.d.ts +137 -0
- package/dist/cjs/mimeparser.js +727 -0
- package/dist/cjs/mock-client.d.ts +39 -0
- package/dist/cjs/mock-client.js +236 -0
- package/dist/cjs/numbers.d.ts +28 -0
- package/dist/cjs/numbers.js +52 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/plugins/acl.d.ts +2 -0
- package/dist/cjs/plugins/acl.js +888 -0
- package/dist/cjs/plugins/appendlimit.d.ts +16 -0
- package/dist/cjs/plugins/appendlimit.js +87 -0
- package/dist/cjs/plugins/auth-plain.d.ts +2 -0
- package/dist/cjs/plugins/auth-plain.js +89 -0
- package/dist/cjs/plugins/binary.d.ts +30 -0
- package/dist/cjs/plugins/binary.js +247 -0
- package/dist/cjs/plugins/catenate.d.ts +14 -0
- package/dist/cjs/plugins/catenate.js +250 -0
- package/dist/cjs/plugins/compress.d.ts +7 -0
- package/dist/cjs/plugins/compress.js +79 -0
- package/dist/cjs/plugins/condstore.d.ts +5 -0
- package/dist/cjs/plugins/condstore.js +492 -0
- package/dist/cjs/plugins/context-search.d.ts +8 -0
- package/dist/cjs/plugins/context-search.js +304 -0
- package/dist/cjs/plugins/context-sort.d.ts +6 -0
- package/dist/cjs/plugins/context-sort.js +32 -0
- package/dist/cjs/plugins/create-special-use.d.ts +7 -0
- package/dist/cjs/plugins/create-special-use.js +101 -0
- package/dist/cjs/plugins/enable.d.ts +23 -0
- package/dist/cjs/plugins/enable.js +127 -0
- package/dist/cjs/plugins/esearch.d.ts +6 -0
- package/dist/cjs/plugins/esearch.js +142 -0
- package/dist/cjs/plugins/esort.d.ts +6 -0
- package/dist/cjs/plugins/esort.js +60 -0
- package/dist/cjs/plugins/id.d.ts +5 -0
- package/dist/cjs/plugins/id.js +123 -0
- package/dist/cjs/plugins/idle.d.ts +5 -0
- package/dist/cjs/plugins/idle.js +85 -0
- package/dist/cjs/plugins/imap4rev2.d.ts +6 -0
- package/dist/cjs/plugins/imap4rev2.js +184 -0
- package/dist/cjs/plugins/index.d.ts +2 -0
- package/dist/cjs/plugins/index.js +119 -0
- package/dist/cjs/plugins/list-extended.d.ts +2 -0
- package/dist/cjs/plugins/list-extended.js +235 -0
- package/dist/cjs/plugins/list-status.d.ts +6 -0
- package/dist/cjs/plugins/list-status.js +42 -0
- package/dist/cjs/plugins/literalminus.d.ts +8 -0
- package/dist/cjs/plugins/literalminus.js +32 -0
- package/dist/cjs/plugins/literalplus.d.ts +6 -0
- package/dist/cjs/plugins/literalplus.js +30 -0
- package/dist/cjs/plugins/logindisabled.d.ts +5 -0
- package/dist/cjs/plugins/logindisabled.js +54 -0
- package/dist/cjs/plugins/messagelimit.d.ts +19 -0
- package/dist/cjs/plugins/messagelimit.js +227 -0
- package/dist/cjs/plugins/metadata-server.d.ts +7 -0
- package/dist/cjs/plugins/metadata-server.js +24 -0
- package/dist/cjs/plugins/metadata.d.ts +11 -0
- package/dist/cjs/plugins/metadata.js +436 -0
- package/dist/cjs/plugins/move.d.ts +11 -0
- package/dist/cjs/plugins/move.js +96 -0
- package/dist/cjs/plugins/multiappend.d.ts +12 -0
- package/dist/cjs/plugins/multiappend.js +38 -0
- package/dist/cjs/plugins/multisearch.d.ts +6 -0
- package/dist/cjs/plugins/multisearch.js +263 -0
- package/dist/cjs/plugins/namespace.d.ts +5 -0
- package/dist/cjs/plugins/namespace.js +57 -0
- package/dist/cjs/plugins/notify.d.ts +2 -0
- package/dist/cjs/plugins/notify.js +628 -0
- package/dist/cjs/plugins/oauthbearer.d.ts +32 -0
- package/dist/cjs/plugins/oauthbearer.js +202 -0
- package/dist/cjs/plugins/objectid.d.ts +23 -0
- package/dist/cjs/plugins/objectid.js +221 -0
- package/dist/cjs/plugins/partial.d.ts +6 -0
- package/dist/cjs/plugins/partial.js +78 -0
- package/dist/cjs/plugins/preview.d.ts +31 -0
- package/dist/cjs/plugins/preview.js +378 -0
- package/dist/cjs/plugins/qresync.d.ts +11 -0
- package/dist/cjs/plugins/qresync.js +473 -0
- package/dist/cjs/plugins/quota.d.ts +18 -0
- package/dist/cjs/plugins/quota.js +254 -0
- package/dist/cjs/plugins/replace.d.ts +14 -0
- package/dist/cjs/plugins/replace.js +130 -0
- package/dist/cjs/plugins/sasl-ir.d.ts +5 -0
- package/dist/cjs/plugins/sasl-ir.js +24 -0
- package/dist/cjs/plugins/savedate.d.ts +17 -0
- package/dist/cjs/plugins/savedate.js +64 -0
- package/dist/cjs/plugins/savelimit.d.ts +10 -0
- package/dist/cjs/plugins/savelimit.js +30 -0
- package/dist/cjs/plugins/searchres.d.ts +6 -0
- package/dist/cjs/plugins/searchres.js +91 -0
- package/dist/cjs/plugins/sort-display.d.ts +11 -0
- package/dist/cjs/plugins/sort-display.js +37 -0
- package/dist/cjs/plugins/sort.d.ts +11 -0
- package/dist/cjs/plugins/sort.js +125 -0
- package/dist/cjs/plugins/special-use.d.ts +10 -0
- package/dist/cjs/plugins/special-use.js +95 -0
- package/dist/cjs/plugins/starttls.d.ts +5 -0
- package/dist/cjs/plugins/starttls.js +55 -0
- package/dist/cjs/plugins/status-size.d.ts +7 -0
- package/dist/cjs/plugins/status-size.js +30 -0
- package/dist/cjs/plugins/thread-orderedsubject.d.ts +11 -0
- package/dist/cjs/plugins/thread-orderedsubject.js +28 -0
- package/dist/cjs/plugins/thread-references.d.ts +11 -0
- package/dist/cjs/plugins/thread-references.js +28 -0
- package/dist/cjs/plugins/uidonly.d.ts +15 -0
- package/dist/cjs/plugins/uidonly.js +126 -0
- package/dist/cjs/plugins/uidplus.d.ts +15 -0
- package/dist/cjs/plugins/uidplus.js +119 -0
- package/dist/cjs/plugins/unauthenticate.d.ts +7 -0
- package/dist/cjs/plugins/unauthenticate.js +32 -0
- package/dist/cjs/plugins/unselect.d.ts +5 -0
- package/dist/cjs/plugins/unselect.js +36 -0
- package/dist/cjs/plugins/utf8-accept.d.ts +6 -0
- package/dist/cjs/plugins/utf8-accept.js +72 -0
- package/dist/cjs/plugins/x-gm-ext-1.d.ts +26 -0
- package/dist/cjs/plugins/x-gm-ext-1.js +422 -0
- package/dist/cjs/plugins/xoauth2.d.ts +2 -0
- package/dist/cjs/plugins/xoauth2.js +155 -0
- package/dist/cjs/plugins/xtoybird.d.ts +2 -0
- package/dist/cjs/plugins/xtoybird.js +236 -0
- package/dist/cjs/server.d.ts +876 -0
- package/dist/cjs/server.js +2597 -0
- package/dist/cjs/smtp-listener.d.ts +13 -0
- package/dist/cjs/smtp-listener.js +47 -0
- package/dist/cjs/sorting.d.ts +77 -0
- package/dist/cjs/sorting.js +290 -0
- package/dist/cjs/threading.d.ts +30 -0
- package/dist/cjs/threading.js +318 -0
- package/dist/cjs/types.d.ts +327 -0
- package/dist/cjs/types.js +3 -0
- package/dist/cjs/utf8-session.d.ts +17 -0
- package/dist/cjs/utf8-session.js +148 -0
- package/dist/cjs/vanished.d.ts +21 -0
- package/dist/cjs/vanished.js +55 -0
- package/dist/esm/addressparser.d.ts +21 -0
- package/{lib → dist/esm}/addressparser.js +102 -123
- package/dist/esm/arguments.d.ts +31 -0
- package/dist/esm/arguments.js +96 -0
- package/dist/esm/bodystructure.d.ts +25 -0
- package/{lib → dist/esm}/bodystructure.js +17 -33
- package/dist/esm/cert.d.ts +2 -0
- package/dist/esm/cert.js +4 -0
- package/dist/esm/command-states.d.ts +36 -0
- package/{lib → dist/esm}/command-states.js +4 -16
- package/dist/esm/commands/append.d.ts +88 -0
- package/{lib → dist/esm}/commands/append.js +47 -85
- package/dist/esm/commands/capability.d.ts +2 -0
- package/dist/esm/commands/capability.js +29 -0
- package/dist/esm/commands/check.d.ts +2 -0
- package/dist/esm/commands/check.js +13 -0
- package/dist/esm/commands/close.d.ts +2 -0
- package/dist/esm/commands/close.js +20 -0
- package/dist/esm/commands/copy.d.ts +30 -0
- package/{lib → dist/esm}/commands/copy.js +31 -55
- package/dist/esm/commands/create.d.ts +2 -0
- package/{lib → dist/esm}/commands/create.js +25 -39
- package/dist/esm/commands/delete.d.ts +2 -0
- package/{lib → dist/esm}/commands/delete.js +25 -41
- package/dist/esm/commands/examine.d.ts +2 -0
- package/dist/esm/commands/examine.js +4 -0
- package/dist/esm/commands/expunge.d.ts +2 -0
- package/dist/esm/commands/expunge.js +17 -0
- package/dist/esm/commands/fetch.d.ts +23 -0
- package/{lib → dist/esm}/commands/fetch.js +33 -74
- package/dist/esm/commands/handlers/fetch.d.ts +12 -0
- package/{lib → dist/esm}/commands/handlers/fetch.js +11 -42
- package/dist/esm/commands/handlers/flags.d.ts +17 -0
- package/{lib → dist/esm}/commands/handlers/flags.js +3 -11
- package/dist/esm/commands/handlers/search.d.ts +85 -0
- package/{lib → dist/esm}/commands/handlers/search.js +33 -71
- package/dist/esm/commands/handlers/status.d.ts +34 -0
- package/{lib → dist/esm}/commands/handlers/status.js +1 -9
- package/dist/esm/commands/handlers/store.d.ts +3 -0
- package/{lib → dist/esm}/commands/handlers/store.js +20 -44
- package/dist/esm/commands/index.d.ts +2 -0
- package/dist/esm/commands/index.js +55 -0
- package/dist/esm/commands/list.d.ts +2 -0
- package/dist/esm/commands/list.js +71 -0
- package/dist/esm/commands/login.d.ts +2 -0
- package/{lib → dist/esm}/commands/login.js +24 -44
- package/dist/esm/commands/logout.d.ts +2 -0
- package/dist/esm/commands/logout.js +25 -0
- package/dist/esm/commands/lsub.d.ts +2 -0
- package/dist/esm/commands/lsub.js +64 -0
- package/dist/esm/commands/noop.d.ts +2 -0
- package/dist/esm/commands/noop.js +13 -0
- package/dist/esm/commands/rename.d.ts +2 -0
- package/{lib → dist/esm}/commands/rename.js +32 -51
- package/dist/esm/commands/search.d.ts +10 -0
- package/dist/esm/commands/search.js +54 -0
- package/dist/esm/commands/select.d.ts +19 -0
- package/dist/esm/commands/select.js +214 -0
- package/dist/esm/commands/status.d.ts +2 -0
- package/{lib → dist/esm}/commands/status.js +18 -39
- package/dist/esm/commands/store.d.ts +10 -0
- package/{lib → dist/esm}/commands/store.js +39 -73
- package/dist/esm/commands/subscribe.d.ts +2 -0
- package/{lib → dist/esm}/commands/subscribe.js +22 -39
- package/dist/esm/commands/uid-copy.d.ts +2 -0
- package/dist/esm/commands/uid-copy.js +4 -0
- package/dist/esm/commands/uid-fetch.d.ts +3 -0
- package/dist/esm/commands/uid-fetch.js +3 -0
- package/dist/esm/commands/uid-search.d.ts +3 -0
- package/dist/esm/commands/uid-search.js +3 -0
- package/dist/esm/commands/uid-store.d.ts +3 -0
- package/dist/esm/commands/uid-store.js +3 -0
- package/dist/esm/commands/unsubscribe.d.ts +2 -0
- package/{lib → dist/esm}/commands/unsubscribe.js +22 -37
- package/dist/esm/dates.d.ts +64 -0
- package/{lib → dist/esm}/dates.js +6 -14
- package/dist/esm/deflate-layer.d.ts +83 -0
- package/{lib → dist/esm}/deflate-layer.js +9 -26
- package/dist/esm/encoded-words.d.ts +15 -0
- package/{lib → dist/esm}/encoded-words.js +9 -25
- package/dist/esm/envelope.d.ts +33 -0
- package/{lib → dist/esm}/envelope.js +8 -19
- package/dist/esm/esearch.d.ts +96 -0
- package/{lib → dist/esm}/esearch.js +12 -39
- package/dist/esm/framing.d.ts +66 -0
- package/dist/esm/framing.js +82 -0
- package/dist/esm/index.d.ts +9 -0
- package/dist/esm/index.js +6 -0
- package/dist/esm/list-extensions.d.ts +50 -0
- package/{lib → dist/esm}/list-extensions.js +1 -4
- package/dist/esm/load-plugins.d.ts +21 -0
- package/{lib → dist/esm}/load-plugins.js +9 -43
- package/dist/esm/mailbox-name.d.ts +26 -0
- package/{lib → dist/esm}/mailbox-name.js +5 -26
- package/dist/esm/mimeparser.d.ts +137 -0
- package/{lib → dist/esm}/mimeparser.js +74 -152
- package/dist/esm/mock-client.d.ts +39 -0
- package/{lib → dist/esm}/mock-client.js +26 -40
- package/dist/esm/numbers.d.ts +28 -0
- package/{lib → dist/esm}/numbers.js +1 -8
- package/dist/esm/package.json +3 -0
- package/dist/esm/plugins/acl.d.ts +2 -0
- package/{lib → dist/esm}/plugins/acl.js +146 -240
- package/dist/esm/plugins/appendlimit.d.ts +16 -0
- package/{lib → dist/esm}/plugins/appendlimit.js +5 -16
- package/dist/esm/plugins/auth-plain.d.ts +2 -0
- package/{lib → dist/esm}/plugins/auth-plain.js +11 -31
- package/dist/esm/plugins/binary.d.ts +30 -0
- package/{lib → dist/esm}/plugins/binary.js +17 -43
- package/dist/esm/plugins/catenate.d.ts +14 -0
- package/{lib → dist/esm}/plugins/catenate.js +19 -40
- package/dist/esm/plugins/compress.d.ts +7 -0
- package/dist/esm/plugins/compress.js +61 -0
- package/dist/esm/plugins/condstore.d.ts +5 -0
- package/{lib → dist/esm}/plugins/condstore.js +66 -152
- package/dist/esm/plugins/context-search.d.ts +8 -0
- package/{lib → dist/esm}/plugins/context-search.js +53 -88
- package/dist/esm/plugins/context-sort.d.ts +6 -0
- package/{lib → dist/esm}/plugins/context-sort.js +4 -9
- package/dist/esm/plugins/create-special-use.d.ts +7 -0
- package/{lib → dist/esm}/plugins/create-special-use.js +33 -55
- package/dist/esm/plugins/enable.d.ts +23 -0
- package/dist/esm/plugins/enable.js +111 -0
- package/dist/esm/plugins/esearch.d.ts +6 -0
- package/{lib → dist/esm}/plugins/esearch.js +13 -42
- package/dist/esm/plugins/esort.d.ts +6 -0
- package/{lib → dist/esm}/plugins/esort.js +7 -25
- package/dist/esm/plugins/id.d.ts +5 -0
- package/dist/esm/plugins/id.js +108 -0
- package/dist/esm/plugins/idle.d.ts +5 -0
- package/dist/esm/plugins/idle.js +70 -0
- package/dist/esm/plugins/imap4rev2.d.ts +6 -0
- package/{lib → dist/esm}/plugins/imap4rev2.js +24 -60
- package/dist/esm/plugins/index.d.ts +2 -0
- package/dist/esm/plugins/index.js +113 -0
- package/dist/esm/plugins/list-extended.d.ts +2 -0
- package/{lib → dist/esm}/plugins/list-extended.js +28 -66
- package/dist/esm/plugins/list-status.d.ts +6 -0
- package/{lib → dist/esm}/plugins/list-status.js +5 -12
- package/dist/esm/plugins/literalminus.d.ts +8 -0
- package/{lib → dist/esm}/plugins/literalminus.js +2 -5
- package/dist/esm/plugins/literalplus.d.ts +6 -0
- package/{lib → dist/esm}/plugins/literalplus.js +2 -5
- package/dist/esm/plugins/logindisabled.d.ts +5 -0
- package/dist/esm/plugins/logindisabled.js +39 -0
- package/dist/esm/plugins/messagelimit.d.ts +19 -0
- package/{lib → dist/esm}/plugins/messagelimit.js +18 -41
- package/dist/esm/plugins/metadata-server.d.ts +7 -0
- package/{lib → dist/esm}/plugins/metadata-server.js +3 -7
- package/dist/esm/plugins/metadata.d.ts +11 -0
- package/dist/esm/plugins/metadata.js +421 -0
- package/dist/esm/plugins/move.d.ts +11 -0
- package/{lib → dist/esm}/plugins/move.js +37 -66
- package/dist/esm/plugins/multiappend.d.ts +12 -0
- package/{lib → dist/esm}/plugins/multiappend.js +2 -5
- package/dist/esm/plugins/multisearch.d.ts +6 -0
- package/{lib → dist/esm}/plugins/multisearch.js +24 -48
- package/dist/esm/plugins/namespace.d.ts +5 -0
- package/dist/esm/plugins/namespace.js +42 -0
- package/dist/esm/plugins/notify.d.ts +2 -0
- package/{lib → dist/esm}/plugins/notify.js +90 -134
- package/dist/esm/plugins/oauthbearer.d.ts +32 -0
- package/{lib → dist/esm}/plugins/oauthbearer.js +18 -48
- package/dist/esm/plugins/objectid.d.ts +23 -0
- package/{lib → dist/esm}/plugins/objectid.js +21 -58
- package/dist/esm/plugins/partial.d.ts +6 -0
- package/{lib → dist/esm}/plugins/partial.js +7 -15
- package/dist/esm/plugins/preview.d.ts +31 -0
- package/{lib → dist/esm}/plugins/preview.js +28 -66
- package/dist/esm/plugins/qresync.d.ts +11 -0
- package/{lib → dist/esm}/plugins/qresync.js +64 -134
- package/dist/esm/plugins/quota.d.ts +18 -0
- package/{lib → dist/esm}/plugins/quota.js +89 -135
- package/dist/esm/plugins/replace.d.ts +14 -0
- package/{lib → dist/esm}/plugins/replace.js +21 -51
- package/dist/esm/plugins/sasl-ir.d.ts +5 -0
- package/{lib → dist/esm}/plugins/sasl-ir.js +3 -6
- package/dist/esm/plugins/savedate.d.ts +17 -0
- package/{lib → dist/esm}/plugins/savedate.js +5 -15
- package/dist/esm/plugins/savelimit.d.ts +10 -0
- package/{lib → dist/esm}/plugins/savelimit.js +3 -6
- package/dist/esm/plugins/searchres.d.ts +6 -0
- package/{lib → dist/esm}/plugins/searchres.js +13 -22
- package/dist/esm/plugins/sort-display.d.ts +11 -0
- package/{lib → dist/esm}/plugins/sort-display.js +6 -10
- package/dist/esm/plugins/sort.d.ts +11 -0
- package/{lib → dist/esm}/plugins/sort.js +24 -46
- package/dist/esm/plugins/special-use.d.ts +10 -0
- package/{lib → dist/esm}/plugins/special-use.js +9 -24
- package/dist/esm/plugins/starttls.d.ts +5 -0
- package/{lib → dist/esm}/plugins/starttls.js +20 -37
- package/dist/esm/plugins/status-size.d.ts +7 -0
- package/{lib → dist/esm}/plugins/status-size.js +4 -11
- package/dist/esm/plugins/thread-orderedsubject.d.ts +11 -0
- package/{lib → dist/esm}/plugins/thread-orderedsubject.js +3 -6
- package/dist/esm/plugins/thread-references.d.ts +11 -0
- package/{lib → dist/esm}/plugins/thread-references.js +3 -6
- package/dist/esm/plugins/uidonly.d.ts +15 -0
- package/{lib → dist/esm}/plugins/uidonly.js +11 -35
- package/dist/esm/plugins/uidplus.d.ts +15 -0
- package/{lib → dist/esm}/plugins/uidplus.js +25 -45
- package/dist/esm/plugins/unauthenticate.d.ts +7 -0
- package/dist/esm/plugins/unauthenticate.js +17 -0
- package/dist/esm/plugins/unselect.d.ts +5 -0
- package/dist/esm/plugins/unselect.js +21 -0
- package/dist/esm/plugins/utf8-accept.d.ts +6 -0
- package/{lib → dist/esm}/plugins/utf8-accept.js +10 -21
- package/dist/esm/plugins/x-gm-ext-1.d.ts +26 -0
- package/{lib → dist/esm}/plugins/x-gm-ext-1.js +48 -100
- package/dist/esm/plugins/xoauth2.d.ts +2 -0
- package/dist/esm/plugins/xoauth2.js +140 -0
- package/dist/esm/plugins/xtoybird.d.ts +2 -0
- package/dist/esm/plugins/xtoybird.js +218 -0
- package/dist/esm/server.d.ts +876 -0
- package/dist/esm/server.js +2543 -0
- package/dist/esm/smtp-listener.d.ts +13 -0
- package/{lib → dist/esm}/smtp-listener.js +5 -12
- package/dist/esm/sorting.d.ts +77 -0
- package/{lib → dist/esm}/sorting.js +40 -53
- package/dist/esm/threading.d.ts +30 -0
- package/{lib → dist/esm}/threading.js +29 -70
- package/dist/esm/types.d.ts +327 -0
- package/dist/esm/types.js +2 -0
- package/dist/esm/utf8-session.d.ts +17 -0
- package/{lib → dist/esm}/utf8-session.js +15 -25
- package/dist/esm/vanished.d.ts +21 -0
- package/{lib → dist/esm}/vanished.js +16 -21
- package/dist/plugin-help.json +298 -0
- package/package.json +54 -11
- package/lib/commands/capability.js +0 -47
- package/lib/commands/check.js +0 -21
- package/lib/commands/close.js +0 -30
- package/lib/commands/examine.js +0 -7
- package/lib/commands/expunge.js +0 -27
- package/lib/commands/list.js +0 -100
- package/lib/commands/logout.js +0 -41
- package/lib/commands/lsub.js +0 -87
- package/lib/commands/noop.js +0 -21
- package/lib/commands/search.js +0 -76
- package/lib/commands/select.js +0 -289
- package/lib/commands/uid copy.js +0 -7
- package/lib/commands/uid fetch.js +0 -5
- package/lib/commands/uid search.js +0 -5
- package/lib/commands/uid store.js +0 -5
- package/lib/plugins/compress.js +0 -76
- package/lib/plugins/enable.js +0 -155
- package/lib/plugins/id.js +0 -138
- package/lib/plugins/idle.js +0 -105
- package/lib/plugins/logindisabled.js +0 -50
- package/lib/plugins/metadata.js +0 -475
- package/lib/plugins/namespace.js +0 -67
- package/lib/plugins/unauthenticate.js +0 -28
- package/lib/plugins/unselect.js +0 -36
- package/lib/plugins/xoauth2.js +0 -188
- package/lib/plugins/xtoybird.js +0 -282
- package/lib/server.js +0 -2880
|
@@ -0,0 +1,2543 @@
|
|
|
1
|
+
import { Stream } from 'node:stream';
|
|
2
|
+
import net from 'node:net';
|
|
3
|
+
import tls from 'node:tls';
|
|
4
|
+
import imapHandler from 'imap-handler';
|
|
5
|
+
import formalSyntax from 'imap-handler/lib/formal';
|
|
6
|
+
import loadPlugins from './load-plugins.js';
|
|
7
|
+
import { commands as builtinCommands } from './commands/index.js';
|
|
8
|
+
import { getCommandOptions, commandOptions } from './command-states.js';
|
|
9
|
+
import validateMailboxName from './mailbox-name.js';
|
|
10
|
+
import { MONTHS, monthIndex, isRealDate } from './dates.js';
|
|
11
|
+
import fetchHandlers from './commands/handlers/fetch.js';
|
|
12
|
+
import { hasSequenceSetKey } from './commands/handlers/search.js';
|
|
13
|
+
import { isSequenceSet } from './numbers.js';
|
|
14
|
+
import { restoreNilAtoms } from './arguments.js';
|
|
15
|
+
import { refuseMissingTarget } from './commands/append.js';
|
|
16
|
+
import * as bundledCert from './cert.js';
|
|
17
|
+
// longest command line (not counting literals) accepted from a client
|
|
18
|
+
const MAX_LINE_LENGTH = 1024 * 1024;
|
|
19
|
+
// largest literal accepted after login, override with the maxLiteralSize option
|
|
20
|
+
const MAX_LITERAL_SIZE = 64 * 1024 * 1024;
|
|
21
|
+
// largest literal accepted before login, enough for any user name or password
|
|
22
|
+
const MAX_PREAUTH_LITERAL_SIZE = 64 * 1024;
|
|
23
|
+
const LITERAL_TOO_LARGE = 'Literal too large';
|
|
24
|
+
// status responses, their text must follow the RFC 3501 section 9 resp-text rules
|
|
25
|
+
const STATUS_RESPONSES = new Set(['OK', 'NO', 'BAD', 'BYE', 'PREAUTH']);
|
|
26
|
+
// RFC 3501 section 9: tag = 1*<any ASTRING-CHAR except "+">
|
|
27
|
+
const TAG_REGEX = new RegExp('^[' + formalSyntax.tag().replace(/[\\\]^-]/g, '\\$&') + ']+$');
|
|
28
|
+
// RFC 3501 section 9: atom = 1*ATOM-CHAR
|
|
29
|
+
const ATOM_CHARS = '[' + formalSyntax['ATOM-CHAR']().replace(/[\\\]^-]/g, '\\$&') + ']+';
|
|
30
|
+
const ATOM_REGEX = new RegExp('^' + ATOM_CHARS + '$');
|
|
31
|
+
// RFC 3501 section 9: a command name, and the second word of UID and AUTHENTICATE, is an atom
|
|
32
|
+
const COMMAND_REGEX = new RegExp('^' + ATOM_CHARS + '( ' + ATOM_CHARS + ')?$');
|
|
33
|
+
// RFC spelling of the mailbox attributes the server checks or computes (RFC 3501 section 7.2.2, RFC 3348
|
|
34
|
+
// section 3, RFC 5258 section 3), keyed by lowercase name
|
|
35
|
+
const MAILBOX_ATTRIBUTES = new Map(['\\Noinferiors', '\\Noselect', '\\Marked', '\\Unmarked', '\\HasChildren', '\\HasNoChildren', '\\NonExistent'].map(flag => [flag.toLowerCase(), flag]));
|
|
36
|
+
/**
|
|
37
|
+
* Returns the tag to use when answering a raw command line that could not be parsed. A line
|
|
38
|
+
* without a valid tag is answered untagged, a client could not parse the invalid tag anyway.
|
|
39
|
+
*
|
|
40
|
+
* @param {String} line Raw command line
|
|
41
|
+
* @return {String} tag or "*"
|
|
42
|
+
*/
|
|
43
|
+
function getResponseTag(line) {
|
|
44
|
+
// only SP separates the tag (RFC 3501 section 9: command = tag SP ...)
|
|
45
|
+
const space = line.indexOf(' ');
|
|
46
|
+
const tag = space >= 0 ? line.substr(0, space) : line;
|
|
47
|
+
return tag && TAG_REGEX.test(tag) ? tag : '*';
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Text for a command that is not valid in the current connection state
|
|
51
|
+
*
|
|
52
|
+
* @param {String} command Upper case command name
|
|
53
|
+
* @param {String} state Connection state
|
|
54
|
+
* @return {String} Error text
|
|
55
|
+
*/
|
|
56
|
+
function stateError(command, state) {
|
|
57
|
+
return command + ' is not allowed in the ' + state + ' state';
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Creates an error for a failed mailbox operation, with a RFC 5530 response code
|
|
61
|
+
*
|
|
62
|
+
* @param {String} message Error message
|
|
63
|
+
* @param {String} code Response code, e.g. "ALREADYEXISTS"
|
|
64
|
+
* @return {Error} Error object
|
|
65
|
+
*/
|
|
66
|
+
function mailboxError(message, code) {
|
|
67
|
+
const err = new Error(message);
|
|
68
|
+
err.code = code;
|
|
69
|
+
return err;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Creates a new IMAP server, call `listen()` on it to start accepting connections
|
|
73
|
+
*
|
|
74
|
+
* @param options Server options, the mailbox tree comes from `options.storage`
|
|
75
|
+
* @return Server instance
|
|
76
|
+
*/
|
|
77
|
+
export default function server(options) {
|
|
78
|
+
return new IMAPServer(options);
|
|
79
|
+
}
|
|
80
|
+
class IMAPServer extends Stream {
|
|
81
|
+
constructor(options) {
|
|
82
|
+
super();
|
|
83
|
+
// shallow copy, so that the caller's options object is never modified
|
|
84
|
+
this.options = Object.assign({}, options);
|
|
85
|
+
if (this.options.secureConnection) {
|
|
86
|
+
this.server = tls.createServer(this.getCredentials(), this.createClient.bind(this));
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
this.server = net.createServer(this.createClient.bind(this));
|
|
90
|
+
}
|
|
91
|
+
// every connection listens to the notify event
|
|
92
|
+
this.setMaxListeners(0);
|
|
93
|
+
this.connections = new Set();
|
|
94
|
+
this.connectionHandlers = [];
|
|
95
|
+
// run when a connection returns to the Not Authenticated state (UNAUTHENTICATE), each one
|
|
96
|
+
// clears the per-session state its plugin keeps on the connection
|
|
97
|
+
this.resetHandlers = [];
|
|
98
|
+
this.outputHandlers = [];
|
|
99
|
+
this.messageHandlers = [];
|
|
100
|
+
this.fetchHandlers = {};
|
|
101
|
+
this.fetchFilters = [];
|
|
102
|
+
this.searchHandlers = {};
|
|
103
|
+
this.storeHandlers = {};
|
|
104
|
+
this.storeFilters = [];
|
|
105
|
+
// `filter(connection, notification)` functions, a notification only reaches connections they all accept
|
|
106
|
+
this.notifyFilters = [];
|
|
107
|
+
// run on every mailbox in processMailbox, like messageHandlers for messages
|
|
108
|
+
this.mailboxHandlers = [];
|
|
109
|
+
// consulted before messages are added to a mailbox by APPEND, COPY or MOVE, see IMAPConnection#checkAppend
|
|
110
|
+
this.appendChecks = [];
|
|
111
|
+
// carry properties over when a message is copied to another mailbox (COPY, MOVE, RENAME INBOX), see copyMessage
|
|
112
|
+
this.copyHandlers = [];
|
|
113
|
+
// append-data extensions such as CATENATE (RFC 4466 section 2.7), and checks that can refuse
|
|
114
|
+
// a synchronizing literal before it is read, see IMAPConnection#checkLiteral
|
|
115
|
+
this.appendDataHandlers = Object.create(null);
|
|
116
|
+
// APPEND and REPLACE to a mailbox that does not exist are refused before the message is sent
|
|
117
|
+
this.literalFilters = [refuseMissingTarget];
|
|
118
|
+
// can refuse IMAP URLs that read a mailbox (CATENATE), `(connection, mailbox, url)` returns `{ text }` to refuse
|
|
119
|
+
this.urlAccessChecks = [];
|
|
120
|
+
// can leave mailboxes out of a search of several mailboxes (ESEARCH of MULTISEARCH), `(connection, mailbox, named)`
|
|
121
|
+
// returns false for a mailbox that is skipped, `named` is true if the client gave its name
|
|
122
|
+
this.searchAccessChecks = [];
|
|
123
|
+
// run before a command handler, `(connection, parsed)` returns `{ command, code, text }` to refuse the command
|
|
124
|
+
// (e.g. commands with message sequence numbers after ENABLE UIDONLY), see IMAPConnection#processQueue
|
|
125
|
+
this.commandChecks = [];
|
|
126
|
+
// `(connection, parsed, range)` functions that can cut the messages that FETCH, STORE, COPY, MOVE and UID EXPUNGE
|
|
127
|
+
// (and the UID variants) operate on, e.g. MESSAGELIMIT. See IMAPConnection#limitRange
|
|
128
|
+
this.rangeLimits = [];
|
|
129
|
+
// `(connection, messages, query)` functions that can narrow down the messages a SEARCH (or SORT, THREAD) looks
|
|
130
|
+
// at by returning a shorter list, e.g. MESSAGELIMIT. See commands/handlers/search.ts
|
|
131
|
+
this.searchLimits = [];
|
|
132
|
+
// `check(connection)` functions, SELECT and EXAMINE send `* OK [CLOSED]` when they close the selected mailbox
|
|
133
|
+
// if any of them is true (CONDSTORE, RFC 7162 section 3.2.11, IMAP4rev2, RFC 9051 section 6.3.2)
|
|
134
|
+
this.closedChecks = [];
|
|
135
|
+
// set by MULTIAPPEND (RFC 3502), otherwise APPEND takes a single message
|
|
136
|
+
this.multiAppend = false;
|
|
137
|
+
// the built-in handlers, setCommandHandler replaces them. Without a prototype, a command name like
|
|
138
|
+
// "TOSTRING" never finds an inherited function
|
|
139
|
+
this.commandHandlers = Object.assign(Object.create(null), builtinCommands);
|
|
140
|
+
// options of commands that plugins add, core commands are listed in command-states.ts
|
|
141
|
+
this.commandOptions = Object.create(null);
|
|
142
|
+
this.capabilities = {};
|
|
143
|
+
this.allowedStatus = ['MESSAGES', 'RECENT', 'UIDNEXT', 'UIDVALIDITY', 'UNSEEN'];
|
|
144
|
+
// values of STATUS items that plugins add, consulted before the built-in items in commands/handlers/status.ts
|
|
145
|
+
this.statusHandlers = {};
|
|
146
|
+
// non-synchronizing literals {n+} are accepted when literalPlus is set (LITERAL+ and LITERAL-),
|
|
147
|
+
// up to nonSyncLiteralLimit octets (4096 for LITERAL-, RFC 7888 section 5)
|
|
148
|
+
this.literalPlus = false;
|
|
149
|
+
this.nonSyncLiteralLimit = Infinity;
|
|
150
|
+
// extra options for the imap-handler command parser, e.g. literal8 for BINARY
|
|
151
|
+
this.parserOptions = {
|
|
152
|
+
// items that take a [section] and <partial>, the imap-handler default
|
|
153
|
+
allowSection: ['BODY', 'BODY.PEEK']
|
|
154
|
+
};
|
|
155
|
+
this.referenceNamespace = false;
|
|
156
|
+
// the session whose command is running, see IMAPServer#notify
|
|
157
|
+
this.activeConnection = null;
|
|
158
|
+
// users and storage are deep copied, so that runtime changes never leak into
|
|
159
|
+
// the caller's objects or into other servers built from the same fixture.
|
|
160
|
+
// Without a prototype, user names like "__proto__" or "toString" are plain keys
|
|
161
|
+
this.users = Object.assign(Object.create(null), this.options.users
|
|
162
|
+
? structuredClone(this.options.users)
|
|
163
|
+
: {
|
|
164
|
+
testuser: {
|
|
165
|
+
password: 'testpass',
|
|
166
|
+
xoauth2: {
|
|
167
|
+
accessToken: 'testtoken',
|
|
168
|
+
sessionTimeout: 3600 * 1000
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
loadPlugins(this, this.options.plugins);
|
|
173
|
+
this.systemFlags = [].concat(this.options.systemFlags || ['\\Answered', '\\Flagged', '\\Draft', '\\Deleted', '\\Seen']);
|
|
174
|
+
// indexFolders() below turns the storage option into namespaces and mailboxes in place
|
|
175
|
+
this.storage = (this.options.storage
|
|
176
|
+
? structuredClone(this.options.storage)
|
|
177
|
+
: {
|
|
178
|
+
INBOX: {},
|
|
179
|
+
'': {}
|
|
180
|
+
});
|
|
181
|
+
this.uidvalidityCounter = 0; // highest UIDVALIDITY in use, new mailboxes get a higher one
|
|
182
|
+
// subscribed mailbox names (RFC 3501 section 6.3.6). Names, not mailboxes: a subscription outlives
|
|
183
|
+
// DELETE and stays with the old name on RENAME (RFC 9051 section 6.3.6), see trackSubscription
|
|
184
|
+
this.subscriptions = new Set();
|
|
185
|
+
this.folderCache = Object.create(null);
|
|
186
|
+
this.indexFolders(true);
|
|
187
|
+
}
|
|
188
|
+
listen(...args) {
|
|
189
|
+
this.server.listen.apply(this.server, args);
|
|
190
|
+
}
|
|
191
|
+
close(callback) {
|
|
192
|
+
this.server.close(callback);
|
|
193
|
+
// close() only completes once all connections are gone
|
|
194
|
+
this.connections.forEach((connection) => {
|
|
195
|
+
if (connection.socket) {
|
|
196
|
+
connection.socket.destroy();
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Returns TLS key and certificate. Without credentials in the options the
|
|
202
|
+
* bundled self-signed certificate for localhost is used.
|
|
203
|
+
*
|
|
204
|
+
* @return {Object} TLS options
|
|
205
|
+
*/
|
|
206
|
+
getCredentials() {
|
|
207
|
+
if (!this.options.credentials) {
|
|
208
|
+
this.options.credentials = {
|
|
209
|
+
key: bundledCert.key,
|
|
210
|
+
cert: bundledCert.cert
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
return this.options.credentials;
|
|
214
|
+
}
|
|
215
|
+
address() {
|
|
216
|
+
return this.server.address();
|
|
217
|
+
}
|
|
218
|
+
createClient(socket) {
|
|
219
|
+
const connection = new IMAPConnection(this, socket);
|
|
220
|
+
this.connectionHandlers.forEach(handler => {
|
|
221
|
+
handler(connection);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
registerCapability(keyword, handler) {
|
|
225
|
+
this.capabilities[keyword] =
|
|
226
|
+
handler ||
|
|
227
|
+
function () {
|
|
228
|
+
return true;
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Sets the handler of a command
|
|
233
|
+
*
|
|
234
|
+
* @param {String} command Command name, e.g. "UID MOVE"
|
|
235
|
+
* @param {Function} handler Command handler `(connection, parsed, data, callback)`
|
|
236
|
+
* @param {Object|Array} [options] `{ states, noArguments, mailboxArguments, astringArguments, searchCriteria, sequenceSet,
|
|
237
|
+
* noExpunge, literal8, noPipelining }`: the connection states the command is valid in (any state if not set), if it takes
|
|
238
|
+
* no arguments, the positions of its mailbox name arguments and of its other astring arguments, the position where its search criteria start, the position of its argument
|
|
239
|
+
* with message sequence numbers, if EXPUNGE responses
|
|
240
|
+
* are not allowed while it runs, if it accepts literal8 arguments (true, or the name of the capability that
|
|
241
|
+
* allows them), and if it is refused when the client sent more input after it. A list is read as the states.
|
|
242
|
+
* Without options, a command keeps its earlier settings
|
|
243
|
+
*/
|
|
244
|
+
setCommandHandler(command, handler, options) {
|
|
245
|
+
command = (command || '').toString().toUpperCase();
|
|
246
|
+
this.commandHandlers[command] = handler;
|
|
247
|
+
if (options) {
|
|
248
|
+
this.commandOptions[command] = commandOptions(options);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Returns the options of a command, see setCommandHandler
|
|
253
|
+
*
|
|
254
|
+
* @param {String} command Command name
|
|
255
|
+
* @return {Object} the command options, see setCommandHandler, states is false if any state is fine
|
|
256
|
+
*/
|
|
257
|
+
getCommandOptions(command) {
|
|
258
|
+
command = (command || '').toString().toUpperCase();
|
|
259
|
+
return this.commandOptions[command] || getCommandOptions(command) || commandOptions();
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Returns the connection states a command may be used in. Public API for custom plugins (see README), the server
|
|
263
|
+
* itself reads getCommandOptions
|
|
264
|
+
*
|
|
265
|
+
* @param {String} command Command name
|
|
266
|
+
* @return {Array|Boolean} List of states, or false if any state is fine
|
|
267
|
+
*/
|
|
268
|
+
getCommandStates(command) {
|
|
269
|
+
return this.getCommandOptions(command).states;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Returns a user account
|
|
273
|
+
*
|
|
274
|
+
* @param {String} username User name
|
|
275
|
+
* @return {Object|false} User data or false if there is no such user
|
|
276
|
+
*/
|
|
277
|
+
getUser(username) {
|
|
278
|
+
return (typeof username === 'string' && this.users[username]) || false;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Returns a mailbox object from folderCache
|
|
282
|
+
*
|
|
283
|
+
* @param {String} path Pathname for the mailbox
|
|
284
|
+
* @return {Object} mailbox object or undefined
|
|
285
|
+
*/
|
|
286
|
+
getMailbox(path) {
|
|
287
|
+
if (path.toUpperCase() === 'INBOX') {
|
|
288
|
+
return this.folderCache.INBOX;
|
|
289
|
+
}
|
|
290
|
+
return this.folderCache[path];
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Schedules a notifying message
|
|
294
|
+
*
|
|
295
|
+
* @param {Object} command An object of untagged response message
|
|
296
|
+
* @param {Object|String} mailbox Mailbox the message is related to
|
|
297
|
+
* @param {Object} ignoreConnection if set the selected connection ignores this notification
|
|
298
|
+
* @param {Function} [filter] if set, only connections for which `filter(connection)` is true get the notification
|
|
299
|
+
*/
|
|
300
|
+
notify(command, mailbox, ignoreConnection, filter) {
|
|
301
|
+
command.notification = true;
|
|
302
|
+
const event = {
|
|
303
|
+
command: command,
|
|
304
|
+
mailbox: mailbox,
|
|
305
|
+
ignoreConnection: ignoreConnection,
|
|
306
|
+
filter: filter,
|
|
307
|
+
// the session whose command caused the change, null for changes from outside (e.g. SMTP)
|
|
308
|
+
origin: this.activeConnection
|
|
309
|
+
};
|
|
310
|
+
this.emit('notify', event);
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Tells plugins that a mailbox was created, deleted, renamed, subscribed or unsubscribed, with a
|
|
314
|
+
* `mailbox` event: `{ type, path, oldPath, mailbox, origin }`. `type` is "create", "delete", "rename",
|
|
315
|
+
* "subscribe" or "unsubscribe", `origin` is the session that made the change
|
|
316
|
+
*
|
|
317
|
+
* @param {String} type Kind of change
|
|
318
|
+
* @param {String} path Storage name of the mailbox
|
|
319
|
+
* @param {Object} [details] `{ oldPath, mailbox }`: the earlier name of a renamed mailbox, the mailbox
|
|
320
|
+
* object that a DELETE removed
|
|
321
|
+
*/
|
|
322
|
+
mailboxChanged(type, path, details) {
|
|
323
|
+
const event = Object.assign({ type, path, oldPath: null, mailbox: null }, details, { origin: this.activeConnection });
|
|
324
|
+
this.emit('mailbox', event);
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Retrieves the handler of an IMAP command
|
|
328
|
+
*
|
|
329
|
+
* @param {String} command Command name
|
|
330
|
+
* @return {Function} handler for the specified command
|
|
331
|
+
*/
|
|
332
|
+
getCommandHandler(command) {
|
|
333
|
+
return this.commandHandlers[(command || '').toString().toUpperCase()] || false;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Returns some useful information about a mailbox that can be used with STATUS, SELECT and EXAMINE
|
|
337
|
+
*
|
|
338
|
+
* @param {Object|String} mailbox Mailbox object or path
|
|
339
|
+
*/
|
|
340
|
+
getStatus(path) {
|
|
341
|
+
const mailbox = typeof path === 'string' ? this.getMailbox(path) : path;
|
|
342
|
+
if (!mailbox) {
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
const flags = {};
|
|
346
|
+
let seen = 0;
|
|
347
|
+
let unseen = 0;
|
|
348
|
+
// flags stay defined in the mailbox once a message had them, see rememberFlags
|
|
349
|
+
const permanentFlags = [].concat(mailbox.permanentFlags || []);
|
|
350
|
+
(mailbox.knownFlags || []).forEach(flag => this.ensureFlag(permanentFlags, flag));
|
|
351
|
+
let recent = 0;
|
|
352
|
+
// \Recent sets of the sessions that have this mailbox selected
|
|
353
|
+
const recentSets = [];
|
|
354
|
+
this.connections.forEach(connection => {
|
|
355
|
+
if (connection.selectedMailbox === mailbox && connection.recent) {
|
|
356
|
+
recentSets.push(connection.recent);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
mailbox.messages.forEach(message => {
|
|
360
|
+
if (message.flags.indexOf('\\Seen') < 0) {
|
|
361
|
+
unseen++;
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
seen++;
|
|
365
|
+
}
|
|
366
|
+
if (message.recent || recentSets.some(set => set.has(message))) {
|
|
367
|
+
recent++;
|
|
368
|
+
}
|
|
369
|
+
message.flags.forEach(flag => {
|
|
370
|
+
if (!flags[flag]) {
|
|
371
|
+
flags[flag] = 1;
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
flags[flag]++;
|
|
375
|
+
}
|
|
376
|
+
if (permanentFlags.indexOf(flag) < 0) {
|
|
377
|
+
permanentFlags.push(flag);
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
return {
|
|
382
|
+
flags: flags,
|
|
383
|
+
seen: seen,
|
|
384
|
+
unseen: unseen,
|
|
385
|
+
recent: recent,
|
|
386
|
+
permanentFlags: permanentFlags
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Validates a date value. Useful for validating APPEND dates
|
|
391
|
+
*
|
|
392
|
+
* @param {String} date Date value to be validated
|
|
393
|
+
* @return {Boolean} Returns true if the date string is in IMAP date-time format
|
|
394
|
+
*/
|
|
395
|
+
validateInternalDate(date) {
|
|
396
|
+
if (!date || typeof date !== 'string') {
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
// date-time from RFC 3501 section 9, month names are case-insensitive like all ABNF strings
|
|
400
|
+
const match = date.match(/^( \d|\d\d)-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-(\d{4}) (\d{2}):(\d{2}):(\d{2}) [-+](\d{2})(\d{2})$/i);
|
|
401
|
+
if (!match) {
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
// the values must also make a real date and time
|
|
405
|
+
return (isRealDate(match[1], monthIndex(match[2]), match[3]) &&
|
|
406
|
+
Number(match[4]) < 24 &&
|
|
407
|
+
Number(match[5]) < 60 &&
|
|
408
|
+
Number(match[6]) < 61 &&
|
|
409
|
+
Number(match[8]) < 60);
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Converts a date object to a valid date-time string format
|
|
413
|
+
*
|
|
414
|
+
* @param {Object} date Date object to be converted
|
|
415
|
+
* @return {String} Returns a valid date-time formatted string
|
|
416
|
+
*/
|
|
417
|
+
formatInternalDate(date) {
|
|
418
|
+
const day = date.getDate();
|
|
419
|
+
const month = MONTHS[date.getMonth()];
|
|
420
|
+
const year = date.getFullYear();
|
|
421
|
+
const hour = date.getHours();
|
|
422
|
+
const minute = date.getMinutes();
|
|
423
|
+
const second = date.getSeconds();
|
|
424
|
+
const tz = date.getTimezoneOffset();
|
|
425
|
+
const tzHours = Math.floor(Math.abs(tz) / 60);
|
|
426
|
+
const tzMins = Math.abs(tz) % 60;
|
|
427
|
+
return ((day < 10 ? '0' : '') +
|
|
428
|
+
day +
|
|
429
|
+
'-' +
|
|
430
|
+
month +
|
|
431
|
+
'-' +
|
|
432
|
+
year +
|
|
433
|
+
' ' +
|
|
434
|
+
(hour < 10 ? '0' : '') +
|
|
435
|
+
hour +
|
|
436
|
+
':' +
|
|
437
|
+
(minute < 10 ? '0' : '') +
|
|
438
|
+
minute +
|
|
439
|
+
':' +
|
|
440
|
+
(second < 10 ? '0' : '') +
|
|
441
|
+
second +
|
|
442
|
+
' ' +
|
|
443
|
+
(tz > 0 ? '-' : '+') +
|
|
444
|
+
(tzHours < 10 ? '0' : '') +
|
|
445
|
+
tzHours +
|
|
446
|
+
(tzMins < 10 ? '0' : '') +
|
|
447
|
+
tzMins);
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Creates a mailbox with specified path
|
|
451
|
+
*
|
|
452
|
+
* @param {String} path Pathname for the mailbox
|
|
453
|
+
* @param {Object} [defaultMailbox] use this object as the mailbox to add instead of empty'
|
|
454
|
+
* @return {Object} the created mailbox
|
|
455
|
+
*/
|
|
456
|
+
createMailbox(path, defaultMailbox) {
|
|
457
|
+
if (!path) {
|
|
458
|
+
throw mailboxError('Invalid mailbox name', 'CANNOT');
|
|
459
|
+
}
|
|
460
|
+
// Ensure case insensitive INBOX
|
|
461
|
+
if (path.toUpperCase() === 'INBOX') {
|
|
462
|
+
throw mailboxError('INBOX can not be modified', 'ALREADYEXISTS');
|
|
463
|
+
}
|
|
464
|
+
const { namespace, storage } = this.getPersonalNamespace(path);
|
|
465
|
+
path = this.stripSeparator(path, storage.separator);
|
|
466
|
+
if (this.folderCache[path] && this.folderCache[path].flags.indexOf('\\Noselect') < 0) {
|
|
467
|
+
throw mailboxError('Mailbox already exists', 'ALREADYEXISTS');
|
|
468
|
+
}
|
|
469
|
+
const folderPath = path.substr(namespace.length).split(storage.separator);
|
|
470
|
+
if (folderPath.some(name => !name)) {
|
|
471
|
+
// an empty hierarchy level ("foo//bar", "/foo", "foo//"). RFC 5530 section 3 has this very case as the
|
|
472
|
+
// example of CANNOT, Dovecot refuses it too
|
|
473
|
+
throw mailboxError('Mailbox names can not have empty hierarchy levels', 'CANNOT');
|
|
474
|
+
}
|
|
475
|
+
let parent = storage;
|
|
476
|
+
let curPath = namespace;
|
|
477
|
+
if (curPath) {
|
|
478
|
+
curPath = curPath.substr(0, curPath.length - storage.separator.length);
|
|
479
|
+
}
|
|
480
|
+
folderPath.forEach(folderName => {
|
|
481
|
+
curPath += (curPath.length ? storage.separator : '') + folderName;
|
|
482
|
+
let folder = this.getMailbox(curPath) || false;
|
|
483
|
+
if (folder && folder.flags && folder.flags.indexOf('\\Noinferiors') >= 0) {
|
|
484
|
+
throw mailboxError('Can not create subfolders for ' + folder.path, 'CANNOT');
|
|
485
|
+
}
|
|
486
|
+
// a \Noselect placeholder that is created again is replaced with a new mailbox that only keeps
|
|
487
|
+
// the children, nothing else of a deleted mailbox may come back (RFC 3501 section 6.3.3)
|
|
488
|
+
const isTarget = curPath === path;
|
|
489
|
+
const useDefault = isTarget && defaultMailbox;
|
|
490
|
+
if (!folder || useDefault || (isTarget && folder.flags.indexOf('\\Noselect') >= 0)) {
|
|
491
|
+
const children = folder && folder.folders;
|
|
492
|
+
// a recreated mailbox must never reuse an earlier UIDVALIDITY value
|
|
493
|
+
const created = useDefault ? defaultMailbox : { uidvalidity: ++this.uidvalidityCounter };
|
|
494
|
+
if (children) {
|
|
495
|
+
created.folders = Object.assign({}, children, created.folders);
|
|
496
|
+
}
|
|
497
|
+
// a new mailbox is subscribed if its name is, a subscription is not part of the mailbox
|
|
498
|
+
this.trackSubscription(created);
|
|
499
|
+
this.processMailbox(curPath, created, namespace);
|
|
500
|
+
parent.folders = parent.folders || {};
|
|
501
|
+
parent.folders[folderName] = created;
|
|
502
|
+
this.folderCache[curPath] = created;
|
|
503
|
+
folder = created;
|
|
504
|
+
}
|
|
505
|
+
if (parent !== storage) {
|
|
506
|
+
// Remove \HasNoChildren and add \\HasChildren from parent. A \Noselect parent stays \Noselect,
|
|
507
|
+
// it already is the hierarchy level the new mailbox needs
|
|
508
|
+
this.setChildrenFlags(parent, true);
|
|
509
|
+
}
|
|
510
|
+
else if (folder.namespace === this.referenceNamespace && this.inboxHoldsNamespace()) {
|
|
511
|
+
this.setChildrenFlags(this.storage.INBOX, true);
|
|
512
|
+
}
|
|
513
|
+
parent = folder;
|
|
514
|
+
});
|
|
515
|
+
return this.folderCache[path];
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Deletes a mailbox with specified path
|
|
519
|
+
*
|
|
520
|
+
* @param {String} path Pathname for the mailbox
|
|
521
|
+
* @param {boolean} keepContents If true do not delete messages
|
|
522
|
+
*/
|
|
523
|
+
deleteMailbox(path, keepContents) {
|
|
524
|
+
// Ensure case insensitive INBOX
|
|
525
|
+
if (path.toUpperCase() === 'INBOX') {
|
|
526
|
+
throw mailboxError('INBOX can not be modified', 'CANNOT');
|
|
527
|
+
}
|
|
528
|
+
const { namespace, storage } = this.getPersonalNamespace(path);
|
|
529
|
+
const mailbox = this.folderCache[this.stripSeparator(path, storage.separator)];
|
|
530
|
+
if (!mailbox) {
|
|
531
|
+
throw mailboxError('Mailbox does not exist', 'NONEXISTENT');
|
|
532
|
+
}
|
|
533
|
+
if (mailbox.flags.indexOf('\\Noselect') >= 0 && Object.keys(mailbox.folders || {}).length) {
|
|
534
|
+
// RFC 9051 section 6.3.5: deleting a \Noselect name that has inferior names is an error, the RFC 5530
|
|
535
|
+
// section 3 HASCHILDREN response code tells the client to delete the children first
|
|
536
|
+
throw mailboxError('Mailbox has children, delete them first', 'HASCHILDREN');
|
|
537
|
+
}
|
|
538
|
+
const levels = mailbox.path.split(storage.separator);
|
|
539
|
+
const folderName = levels.pop();
|
|
540
|
+
const parentKey = levels.join(storage.separator);
|
|
541
|
+
const parent = (parentKey !== 'INBOX' && this.folderCache[parentKey]) || storage;
|
|
542
|
+
if (mailbox.folders && Object.keys(mailbox.folders).length && !keepContents) {
|
|
543
|
+
// Sessions that have the mailbox selected keep the old object, a new SELECT finds the
|
|
544
|
+
// placeholder. The placeholder only keeps the children. Plugin data (MAILBOXID, special-use, metadata, ACL,
|
|
545
|
+
// HIGHESTMODSEQ ...) belongs to the deleted mailbox and must not survive (RFC 3501 section 6.3.4)
|
|
546
|
+
const folder = {
|
|
547
|
+
flags: ['\\Noselect'],
|
|
548
|
+
folders: mailbox.folders
|
|
549
|
+
};
|
|
550
|
+
this.trackSubscription(folder);
|
|
551
|
+
this.processMailbox(mailbox.path, folder, mailbox.namespace);
|
|
552
|
+
parent.folders[folderName] = folder;
|
|
553
|
+
this.folderCache[mailbox.path] = folder;
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
delete this.folderCache[mailbox.path];
|
|
557
|
+
delete parent.folders[folderName];
|
|
558
|
+
if (parent !== storage) {
|
|
559
|
+
if (parent.flags.indexOf('\\Noselect') >= 0 && !Object.keys(parent.folders || {}).length) {
|
|
560
|
+
this.deleteMailbox(parent.path);
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
this.setChildrenFlags(parent, Object.keys(parent.folders || {}).length > 0);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
else if (namespace === this.referenceNamespace && this.inboxHoldsNamespace()) {
|
|
567
|
+
this.setChildrenFlags(this.storage.INBOX, Object.keys(storage.folders || {}).length > 0);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Finds the personal namespace that a new or existing mailbox name belongs to, for CREATE and DELETE
|
|
572
|
+
*
|
|
573
|
+
* @param {String} path Mailbox path
|
|
574
|
+
* @return {Object} `{ namespace, storage }`, the namespace key and its storage object
|
|
575
|
+
* @throws {Error} CANNOT for a namespace prefix or a name in no namespace, NOPERM outside personal namespaces
|
|
576
|
+
*/
|
|
577
|
+
getPersonalNamespace(path) {
|
|
578
|
+
let namespace = '';
|
|
579
|
+
Object.keys(this.storage).forEach(key => {
|
|
580
|
+
if (key === 'INBOX') {
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
const prefix = key.length ? key.substr(0, key.length - this.storage[key].separator.length) : key;
|
|
584
|
+
if (key.length && (path === prefix || path.substr(0, key.length) === key)) {
|
|
585
|
+
if (path === prefix) {
|
|
586
|
+
throw mailboxError('Used mailbox name is a namespace value', 'CANNOT');
|
|
587
|
+
}
|
|
588
|
+
namespace = key;
|
|
589
|
+
}
|
|
590
|
+
else if (!namespace && !key && this.storage[key].type === 'personal') {
|
|
591
|
+
namespace = key;
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
const storage = this.storage[namespace];
|
|
595
|
+
if (!storage) {
|
|
596
|
+
throw mailboxError('Unknown namespace', 'CANNOT');
|
|
597
|
+
}
|
|
598
|
+
if (storage.type !== 'personal') {
|
|
599
|
+
throw mailboxError('Permission denied', 'NOPERM');
|
|
600
|
+
}
|
|
601
|
+
return { namespace, storage };
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Removes a trailing hierarchy separator from a mailbox name, "foo/" names the mailbox "foo"
|
|
605
|
+
*
|
|
606
|
+
* @param {String} path Mailbox path
|
|
607
|
+
* @param {String} separator Hierarchy separator
|
|
608
|
+
* @return {String} path without the separator at the end
|
|
609
|
+
*/
|
|
610
|
+
stripSeparator(path, separator) {
|
|
611
|
+
return path.substr(-separator.length) === separator ? path.substr(0, path.length - separator.length) : path;
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Checks if the personal namespace is below INBOX (e.g. "INBOX."), then the mailboxes of that namespace are
|
|
615
|
+
* the children of INBOX
|
|
616
|
+
*
|
|
617
|
+
* @return {Boolean} true if the reference namespace is INBOX followed by the separator
|
|
618
|
+
*/
|
|
619
|
+
inboxHoldsNamespace() {
|
|
620
|
+
const reference = this.referenceNamespace;
|
|
621
|
+
if (reference === false) {
|
|
622
|
+
return false;
|
|
623
|
+
}
|
|
624
|
+
const namespace = this.storage[reference];
|
|
625
|
+
return !!namespace && reference.substr(0, reference.length - namespace.separator.length).toUpperCase() === 'INBOX';
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Rebuilds folderCache and the path, namespace and flags of every mailbox from storage.
|
|
629
|
+
* INBOX has its own namespace
|
|
630
|
+
*
|
|
631
|
+
* @param {Boolean} [processMessages] If true, messages are prepared as well. Only needed for
|
|
632
|
+
* messages from the initial storage, as message handlers must not run twice for a message
|
|
633
|
+
*/
|
|
634
|
+
indexFolders(processMessages) {
|
|
635
|
+
const folders = Object.create(null);
|
|
636
|
+
const walkTree = (path, separator, branch, namespace) => {
|
|
637
|
+
Object.keys(branch).forEach(key => {
|
|
638
|
+
const curBranch = branch[key];
|
|
639
|
+
const curPath = (path ? path + (path.substr(-1) !== separator ? separator : '') : '') + key;
|
|
640
|
+
this.processMailbox(curPath, curBranch, namespace);
|
|
641
|
+
folders[curPath] = curBranch;
|
|
642
|
+
if (processMessages) {
|
|
643
|
+
this.processMessages(curBranch);
|
|
644
|
+
}
|
|
645
|
+
if (curBranch.folders && Object.keys(curBranch.folders).length) {
|
|
646
|
+
walkTree(curPath, separator, curBranch.folders, namespace);
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
};
|
|
650
|
+
// Ensure INBOX namespace always exists, processMailbox() below makes it a mailbox
|
|
651
|
+
if (!this.storage.INBOX) {
|
|
652
|
+
this.storage.INBOX = {};
|
|
653
|
+
}
|
|
654
|
+
Object.keys(this.storage).forEach(key => {
|
|
655
|
+
if (key !== 'INBOX') {
|
|
656
|
+
this.storage[key].folders = this.storage[key].folders || {};
|
|
657
|
+
// "INBOX." uses "." as the separator, but "#news" does not use "s"
|
|
658
|
+
this.storage[key].separator = this.storage[key].separator || (/[^a-z0-9]$/i.test(key) ? key.substr(-1) : '/');
|
|
659
|
+
this.storage[key].type = this.storage[key].type || 'personal';
|
|
660
|
+
if (this.storage[key].type === 'personal' && this.referenceNamespace === false) {
|
|
661
|
+
this.referenceNamespace = key;
|
|
662
|
+
}
|
|
663
|
+
walkTree(key, this.storage[key].separator, this.storage[key].folders, key);
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
if (!this.referenceNamespace) {
|
|
667
|
+
// the keys are set below
|
|
668
|
+
this.storage[''] = this.storage[''] || {};
|
|
669
|
+
this.storage[''].folders = this.storage[''].folders || {};
|
|
670
|
+
this.storage[''].separator = this.storage[''].separator || '/';
|
|
671
|
+
this.storage[''].type = 'personal';
|
|
672
|
+
this.referenceNamespace = '';
|
|
673
|
+
}
|
|
674
|
+
// referenceNamespace is set by now
|
|
675
|
+
if (!this.storage.INBOX.separator) {
|
|
676
|
+
this.storage.INBOX.separator = this.storage[this.referenceNamespace].separator;
|
|
677
|
+
}
|
|
678
|
+
// INBOX is its own namespace, but its subfolders belong to the personal namespace
|
|
679
|
+
folders.INBOX = this.storage.INBOX;
|
|
680
|
+
this.processMailbox('INBOX', this.storage.INBOX, 'INBOX');
|
|
681
|
+
if (processMessages) {
|
|
682
|
+
this.processMessages(this.storage.INBOX);
|
|
683
|
+
}
|
|
684
|
+
if (this.storage.INBOX.folders && Object.keys(this.storage.INBOX.folders).length) {
|
|
685
|
+
walkTree('INBOX', this.storage.INBOX.separator, this.storage.INBOX.folders, this.referenceNamespace);
|
|
686
|
+
}
|
|
687
|
+
if (this.inboxHoldsNamespace()) {
|
|
688
|
+
this.setChildrenFlags(this.storage.INBOX, Object.keys(this.storage[this.referenceNamespace].folders || {}).length > 0);
|
|
689
|
+
}
|
|
690
|
+
this.folderCache = folders;
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Ensures uid, flags and internaldate for every message of a mailbox and
|
|
694
|
+
* keeps the message list ordered by UID
|
|
695
|
+
*
|
|
696
|
+
* @param {Object} mailbox Mailbox object
|
|
697
|
+
*/
|
|
698
|
+
processMessages(mailbox) {
|
|
699
|
+
const seen = new Set();
|
|
700
|
+
// messages from storage, before they are processed
|
|
701
|
+
const messages = mailbox.messages;
|
|
702
|
+
messages.forEach((entry, i) => {
|
|
703
|
+
// If the input was a raw message, convert it to an object
|
|
704
|
+
const message = typeof entry === 'string'
|
|
705
|
+
? (messages[i] = {
|
|
706
|
+
raw: entry
|
|
707
|
+
})
|
|
708
|
+
: entry;
|
|
709
|
+
this.processMessage(message, mailbox);
|
|
710
|
+
if (seen.has(message.uid)) {
|
|
711
|
+
throw new Error('Duplicate UID ' + message.uid + ' in mailbox ' + mailbox.path);
|
|
712
|
+
}
|
|
713
|
+
seen.add(message.uid);
|
|
714
|
+
});
|
|
715
|
+
mailbox.messages.sort((a, b) => a.uid - b.uid);
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Sets the path, namespace, flags and counters of a mailbox from storage or a new one, and runs the
|
|
719
|
+
* mailbox handlers of plugins on it
|
|
720
|
+
*
|
|
721
|
+
* @param path Mailbox path
|
|
722
|
+
* @param mailbox Mailbox object, completed in place
|
|
723
|
+
* @param namespace Namespace key of the mailbox
|
|
724
|
+
*/
|
|
725
|
+
processMailbox(path, mailbox, namespace) {
|
|
726
|
+
mailbox.path = path;
|
|
727
|
+
mailbox.namespace = namespace;
|
|
728
|
+
mailbox.uid = mailbox.uid || 1;
|
|
729
|
+
mailbox.uidvalidity = mailbox.uidvalidity || 1;
|
|
730
|
+
this.uidvalidityCounter = Math.max(this.uidvalidityCounter, mailbox.uidvalidity);
|
|
731
|
+
// mailbox attributes are case-insensitive (RFC 3501 section 9, note 1), storage may spell them in any case,
|
|
732
|
+
// the checks and responses use the RFC spelling
|
|
733
|
+
mailbox.flags = [].concat(mailbox.flags || []).map(flag => MAILBOX_ATTRIBUTES.get(String(flag).toLowerCase()) || flag);
|
|
734
|
+
mailbox.allowPermanentFlags = 'allowPermanentFlags' in mailbox ? mailbox.allowPermanentFlags : true;
|
|
735
|
+
mailbox.permanentFlags = [].concat(mailbox.permanentFlags || this.systemFlags);
|
|
736
|
+
// a mailbox from storage is subscribed unless it says otherwise
|
|
737
|
+
this.trackSubscription(mailbox, true);
|
|
738
|
+
// ensure message array
|
|
739
|
+
const messages = [].concat(mailbox.messages || []);
|
|
740
|
+
mailbox.messages = messages;
|
|
741
|
+
// ensure highest uidnext
|
|
742
|
+
mailbox.uidnext = Math.max(...[mailbox.uidnext || 1].concat(messages.map(message => {
|
|
743
|
+
// a raw message (string) has no UID yet
|
|
744
|
+
return ((typeof message === 'string' ? 0 : message.uid) || 0) + 1;
|
|
745
|
+
})));
|
|
746
|
+
const processed = mailbox;
|
|
747
|
+
this.setChildrenFlags(processed, Object.keys(processed.folders || {}).length > 0);
|
|
748
|
+
// Allow plugins to process mailboxes
|
|
749
|
+
this.mailboxHandlers.forEach(handler => {
|
|
750
|
+
handler(this, processed);
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Makes `mailbox.subscribed` read and change the subscription of the mailbox name in
|
|
755
|
+
* `server.subscriptions`, so that the subscription stays with the name when the mailbox is deleted
|
|
756
|
+
* or renamed (RFC 3501 section 6.3.6, RFC 9051 section 6.3.6). A `subscribed` value the mailbox
|
|
757
|
+
* already has (from storage) is moved over to the subscription list.
|
|
758
|
+
*
|
|
759
|
+
* @param {Object} mailbox Mailbox object, its `path` is read whenever the subscription is used
|
|
760
|
+
* @param {Boolean} [defaultValue] Subscription of a mailbox without a `subscribed` value. If not
|
|
761
|
+
* set, the subscription list is left as it is
|
|
762
|
+
*/
|
|
763
|
+
trackSubscription(mailbox, defaultValue) {
|
|
764
|
+
const descriptor = Object.getOwnPropertyDescriptor(mailbox, 'subscribed');
|
|
765
|
+
if (descriptor && descriptor.get) {
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
const value = descriptor ? !!descriptor.value : defaultValue;
|
|
769
|
+
Object.defineProperty(mailbox, 'subscribed', {
|
|
770
|
+
enumerable: true,
|
|
771
|
+
configurable: true,
|
|
772
|
+
get: () => this.subscriptions.has(mailbox.path),
|
|
773
|
+
set: (subscribed) => {
|
|
774
|
+
if (subscribed) {
|
|
775
|
+
this.subscriptions.add(mailbox.path);
|
|
776
|
+
}
|
|
777
|
+
else {
|
|
778
|
+
this.subscriptions.delete(mailbox.path);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
if (typeof value === 'boolean') {
|
|
783
|
+
mailbox.subscribed = value;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Sets the stored children attribute of a mailbox (RFC 3348 section 3). A \Noinferiors mailbox gets
|
|
788
|
+
* neither, \Noinferiors already implies \HasNoChildren (RFC 5258 section 3.4)
|
|
789
|
+
*
|
|
790
|
+
* @param {Object} mailbox Mailbox object
|
|
791
|
+
* @param {Boolean} hasChildren true if the mailbox has child mailboxes
|
|
792
|
+
*/
|
|
793
|
+
setChildrenFlags(mailbox, hasChildren) {
|
|
794
|
+
this.removeFlag(mailbox.flags, '\\HasChildren');
|
|
795
|
+
this.removeFlag(mailbox.flags, '\\HasNoChildren');
|
|
796
|
+
if (mailbox.flags.indexOf('\\Noinferiors') < 0) {
|
|
797
|
+
mailbox.flags.push(hasChildren ? '\\HasChildren' : '\\HasNoChildren');
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Ensures that a list of flags includes selected flag
|
|
802
|
+
*
|
|
803
|
+
* @param {Array} flags An array of flags to check
|
|
804
|
+
* @param {String} flag If the flag is missing, add it
|
|
805
|
+
*/
|
|
806
|
+
ensureFlag(flags, flag) {
|
|
807
|
+
if (flags.indexOf(flag) < 0) {
|
|
808
|
+
flags.push(flag);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Removes a flag from a list of flags
|
|
813
|
+
*
|
|
814
|
+
* @param {Array} flags An array of flags to check
|
|
815
|
+
* @param {String} flag If the flag is in the list, remove it
|
|
816
|
+
*/
|
|
817
|
+
removeFlag(flags, flag) {
|
|
818
|
+
let i;
|
|
819
|
+
if (flags.indexOf(flag) >= 0) {
|
|
820
|
+
for (i = flags.length - 1; i >= 0; i--) {
|
|
821
|
+
if (flags[i] === flag) {
|
|
822
|
+
flags.splice(i, 1);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Remembers the flags of a message as flags of the mailbox. A keyword stays in the FLAGS and
|
|
829
|
+
* PERMANENTFLAGS of the mailbox after the last message with it is expunged or loses it, like a
|
|
830
|
+
* keyword a client defined (RFC 3501 section 2.3.2, FLAGS lists the flags applicable for the
|
|
831
|
+
* mailbox, section 7.2.6)
|
|
832
|
+
*
|
|
833
|
+
* @param {Object} mailbox Mailbox object
|
|
834
|
+
* @param {Array} flags Flags of a message
|
|
835
|
+
*/
|
|
836
|
+
rememberFlags(mailbox, flags) {
|
|
837
|
+
const knownFlags = (mailbox.knownFlags = mailbox.knownFlags || []);
|
|
838
|
+
flags.forEach(flag => this.ensureFlag(knownFlags, flag));
|
|
839
|
+
}
|
|
840
|
+
normalizeDateTime(value) {
|
|
841
|
+
if (value instanceof Date) {
|
|
842
|
+
return this.formatInternalDate(value);
|
|
843
|
+
}
|
|
844
|
+
if (typeof value === 'string') {
|
|
845
|
+
// month names are accepted in any case but always sent as "Jan", "Feb", ...
|
|
846
|
+
return value.replace(/-([a-z]{3})-/i, (m, month) => '-' + (MONTHS[monthIndex(month)] || month) + '-');
|
|
847
|
+
}
|
|
848
|
+
return value;
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Sets the UID, flags, internal date and source of a message from storage or a new one, and runs the
|
|
852
|
+
* message handlers of plugins on it
|
|
853
|
+
*
|
|
854
|
+
* @param message Message object, completed in place
|
|
855
|
+
* @param mailbox Mailbox of the message
|
|
856
|
+
*/
|
|
857
|
+
processMessage(message, mailbox) {
|
|
858
|
+
message.internaldate = this.normalizeDateTime(message.internaldate || new Date());
|
|
859
|
+
message.flags = [].concat(message.flags || []);
|
|
860
|
+
if (message.flags.indexOf('\\Recent') >= 0) {
|
|
861
|
+
// \Recent is not a stored flag, it belongs to the first session that selects the mailbox
|
|
862
|
+
this.removeFlag(message.flags, '\\Recent');
|
|
863
|
+
message.recent = true;
|
|
864
|
+
}
|
|
865
|
+
this.rememberFlags(mailbox, message.flags);
|
|
866
|
+
message.uid = message.uid || mailbox.uidnext++;
|
|
867
|
+
if (message.uid >= mailbox.uidnext) {
|
|
868
|
+
mailbox.uidnext = message.uid + 1;
|
|
869
|
+
}
|
|
870
|
+
// message source is kept as a binary string (one character per octet)
|
|
871
|
+
if (message.raw instanceof Uint8Array) {
|
|
872
|
+
message.raw = Buffer.from(message.raw).toString('binary');
|
|
873
|
+
}
|
|
874
|
+
else if (typeof message.raw !== 'string') {
|
|
875
|
+
message.raw = message.raw ? String(message.raw) : '';
|
|
876
|
+
}
|
|
877
|
+
if (/[\u0100-\uffff]/.test(message.raw)) {
|
|
878
|
+
// characters outside Latin-1 can only come from a unicode string, so encode it as UTF-8
|
|
879
|
+
message.raw = Buffer.from(message.raw, 'utf-8').toString('binary');
|
|
880
|
+
}
|
|
881
|
+
// Allow plugins to process messages
|
|
882
|
+
const processed = message;
|
|
883
|
+
this.messageHandlers.forEach(handler => {
|
|
884
|
+
handler(this, processed, mailbox);
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Appends a message to a mailbox
|
|
889
|
+
*
|
|
890
|
+
* @param {Object|String} mailbox Mailbox to append to
|
|
891
|
+
* @param {Array} flags Flags for the message
|
|
892
|
+
* @param {String|Date} internaldate Receive date-time for the message
|
|
893
|
+
* @param {String} raw Message source
|
|
894
|
+
* @param {Object} [ignoreConnection] To not advertise new message to selected connection
|
|
895
|
+
* @param {Object} [properties] More properties of the new message, set before message handlers run
|
|
896
|
+
* @return An object of the form { mailbox, message }
|
|
897
|
+
*/
|
|
898
|
+
appendMessage(path, flags, internaldate, raw, ignoreConnection, properties) {
|
|
899
|
+
const mailbox = typeof path === 'string' ? this.getMailbox(path) : path;
|
|
900
|
+
// processMessage() below sets the UID
|
|
901
|
+
const message = Object.assign({}, properties, {
|
|
902
|
+
flags: flags,
|
|
903
|
+
internaldate: internaldate,
|
|
904
|
+
raw: raw,
|
|
905
|
+
recent: true
|
|
906
|
+
});
|
|
907
|
+
mailbox.messages.push(message);
|
|
908
|
+
this.processMessage(message, mailbox);
|
|
909
|
+
// a session that has the mailbox selected read-write sees the new message as \Recent
|
|
910
|
+
for (const connection of this.connections) {
|
|
911
|
+
if (connection.selectedMailbox === mailbox && !connection.readOnly && connection.recent) {
|
|
912
|
+
connection.recent.add(message);
|
|
913
|
+
delete message.recent;
|
|
914
|
+
break;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
this.notify({
|
|
918
|
+
tag: '*',
|
|
919
|
+
attributes: [
|
|
920
|
+
mailbox.messages.length,
|
|
921
|
+
{
|
|
922
|
+
type: 'ATOM',
|
|
923
|
+
value: 'EXISTS'
|
|
924
|
+
}
|
|
925
|
+
],
|
|
926
|
+
// the new message, for plugins that report more about it (e.g. NOTIFY)
|
|
927
|
+
message: message
|
|
928
|
+
}, mailbox, ignoreConnection);
|
|
929
|
+
return { mailbox: mailbox, message: message };
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Copies a message to a mailbox (COPY, MOVE, RENAME INBOX). The copy is a new message that
|
|
933
|
+
* keeps the flags, internal date and content of the source. `copyHandlers` can carry over more
|
|
934
|
+
* properties of the source, they run before the message handlers see the copy
|
|
935
|
+
*
|
|
936
|
+
* @param {Object} mailbox Target mailbox
|
|
937
|
+
* @param {Object} source Message to copy
|
|
938
|
+
* @return An object of the form { mailbox, message }
|
|
939
|
+
*/
|
|
940
|
+
copyMessage(mailbox, source) {
|
|
941
|
+
const properties = {};
|
|
942
|
+
this.copyHandlers.forEach(handler => {
|
|
943
|
+
handler(this, source, properties, mailbox);
|
|
944
|
+
});
|
|
945
|
+
return this.appendMessage(mailbox, [].concat(source.flags || []), source.internaldate, source.raw, false, properties);
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* Returns the namespace a mailbox path belongs to by its prefix, INBOX not included
|
|
949
|
+
*
|
|
950
|
+
* @param {String} path Mailbox path, it does not have to exist
|
|
951
|
+
* @return {String|Boolean} the longest matching namespace key, or false
|
|
952
|
+
*/
|
|
953
|
+
getNamespace(path) {
|
|
954
|
+
let namespace = false;
|
|
955
|
+
Object.keys(this.storage).forEach(key => {
|
|
956
|
+
if (key !== 'INBOX' && path.substr(0, key.length) === key && (namespace === false || key.length > namespace.length)) {
|
|
957
|
+
namespace = key;
|
|
958
|
+
}
|
|
959
|
+
});
|
|
960
|
+
return namespace;
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Checks if messages can be added to a mailbox (APPEND, COPY, MOVE). TRYCREATE tells the client that CREATE would
|
|
964
|
+
* help (RFC 3501 sections 6.3.11 and 6.4.7), also for a \\Noselect name, which CREATE turns into a mailbox (RFC 9051
|
|
965
|
+
* sections 6.3.12 and 6.4.7: unless it is certain that the target can not be created)
|
|
966
|
+
*
|
|
967
|
+
* @param {String} path Storage name of the target mailbox
|
|
968
|
+
* @return {Object|Boolean} `{ command, code, text }` of the refusal, or false if the mailbox can take messages
|
|
969
|
+
*/
|
|
970
|
+
targetRefusal(path) {
|
|
971
|
+
const mailbox = this.getMailbox(path);
|
|
972
|
+
if (!mailbox) {
|
|
973
|
+
return { command: 'NO', code: 'TRYCREATE', text: 'Target mailbox does not exist' };
|
|
974
|
+
}
|
|
975
|
+
if (mailbox.flags.indexOf('\\Noselect') >= 0) {
|
|
976
|
+
return { command: 'NO', code: 'TRYCREATE', text: 'Target mailbox is not selectable' };
|
|
977
|
+
}
|
|
978
|
+
return false;
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Returns the namespace of a mailbox name: "INBOX" for INBOX, the namespace of the mailbox if it
|
|
982
|
+
* exists (the children of INBOX belong to the personal namespace), otherwise the one of its prefix
|
|
983
|
+
*
|
|
984
|
+
* @param {String|Object} path Mailbox path, it does not have to exist, or a mailbox object
|
|
985
|
+
* @return {String|Boolean} namespace key, or false if the name is in no namespace
|
|
986
|
+
*/
|
|
987
|
+
getMailboxNamespace(path) {
|
|
988
|
+
if (path && typeof path === 'object') {
|
|
989
|
+
return path.namespace;
|
|
990
|
+
}
|
|
991
|
+
if (path.toUpperCase() === 'INBOX') {
|
|
992
|
+
return 'INBOX';
|
|
993
|
+
}
|
|
994
|
+
const mailbox = this.getMailbox(path);
|
|
995
|
+
return mailbox ? mailbox.namespace : this.getNamespace(path);
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* Checks if a mailbox name is in a personal namespace, INBOX included
|
|
999
|
+
*
|
|
1000
|
+
* @param {String|Object} path Mailbox path, it does not have to exist, or a mailbox object
|
|
1001
|
+
* @return {Boolean} true for a personal mailbox
|
|
1002
|
+
*/
|
|
1003
|
+
isPersonal(path) {
|
|
1004
|
+
const key = this.getMailboxNamespace(path);
|
|
1005
|
+
return key === 'INBOX' || (key !== false && !!this.storage[key] && this.storage[key].type === 'personal');
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Returns the hierarchy separator of a mailbox name, from its namespace
|
|
1009
|
+
*
|
|
1010
|
+
* @param {String|Object} path Mailbox path, it does not have to exist, or a mailbox object
|
|
1011
|
+
* @return {String} separator
|
|
1012
|
+
*/
|
|
1013
|
+
getSeparator(path) {
|
|
1014
|
+
const key = this.getMailboxNamespace(path);
|
|
1015
|
+
const namespace = key === false ? undefined : this.storage[key];
|
|
1016
|
+
return (namespace && namespace.separator) || this.storage.INBOX.separator || '/';
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Returns the name one hierarchy level up from a mailbox name. A trailing separator is ignored, the
|
|
1020
|
+
* prefix of a namespace (e.g. "#shared" of "#shared/") is not a mailbox name
|
|
1021
|
+
*
|
|
1022
|
+
* @param {String} path Mailbox path, it does not have to exist
|
|
1023
|
+
* @return {String|Boolean} parent name, "INBOX" for the children of INBOX, or false at the top level
|
|
1024
|
+
*/
|
|
1025
|
+
getParentPath(path) {
|
|
1026
|
+
const separator = this.getSeparator(path);
|
|
1027
|
+
path = this.stripSeparator(path, separator);
|
|
1028
|
+
const index = path.lastIndexOf(separator);
|
|
1029
|
+
if (index <= 0) {
|
|
1030
|
+
return false;
|
|
1031
|
+
}
|
|
1032
|
+
const parent = path.substr(0, index);
|
|
1033
|
+
if (parent.toUpperCase() === 'INBOX') {
|
|
1034
|
+
return 'INBOX';
|
|
1035
|
+
}
|
|
1036
|
+
return this.storage[parent + separator] ? false : parent;
|
|
1037
|
+
}
|
|
1038
|
+
getDescendants(path, folders) {
|
|
1039
|
+
const source = folders || this.folderCache;
|
|
1040
|
+
const prefix = path + this.getSeparator(path);
|
|
1041
|
+
return Object.keys(source)
|
|
1042
|
+
.filter(key => key.substr(0, prefix.length) === prefix)
|
|
1043
|
+
.map(key => source[key]);
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Checks if any mailbox below a mailbox name passes a test, stops at the first one that does
|
|
1047
|
+
*
|
|
1048
|
+
* @param {String} path Mailbox path
|
|
1049
|
+
* @param {Function} predicate `(mailbox)` returns true for a match
|
|
1050
|
+
* @return {Boolean} true if a mailbox below the name matches
|
|
1051
|
+
*/
|
|
1052
|
+
hasDescendant(path, predicate) {
|
|
1053
|
+
const prefix = path + this.getSeparator(path);
|
|
1054
|
+
for (const key of Object.keys(this.folderCache)) {
|
|
1055
|
+
if (key.substr(0, prefix.length) === prefix && predicate(this.folderCache[key])) {
|
|
1056
|
+
return true;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
return false;
|
|
1060
|
+
}
|
|
1061
|
+
/**
|
|
1062
|
+
* Returns the mailbox attributes of a LIST response with computed children attributes (RFC 3348, RFC 5258
|
|
1063
|
+
* section 4), for extended and unsolicited LIST responses. The stored \HasChildren and \HasNoChildren are
|
|
1064
|
+
* replaced, a name that does not exist is \NonExistent (RFC 5258 section 3, it implies \Noselect)
|
|
1065
|
+
*
|
|
1066
|
+
* @param {Object} [mailbox] Mailbox object, can be left out for a name that does not exist
|
|
1067
|
+
* @param {Object} options `{ exists, subscribed, hasChildren, extra }`: false `exists` lists the name as
|
|
1068
|
+
* \NonExistent, `subscribed` adds \Subscribed, `extra` lists more attributes (e.g. \NoAccess)
|
|
1069
|
+
* @return {Array} attribute names
|
|
1070
|
+
*/
|
|
1071
|
+
listAttributes(mailbox, options) {
|
|
1072
|
+
const exists = options.exists !== false;
|
|
1073
|
+
const flags = ((mailbox && mailbox.flags) || []).filter(flag => flag !== '\\HasChildren' && flag !== '\\HasNoChildren' && (exists || flag !== '\\Noselect'));
|
|
1074
|
+
if (!exists) {
|
|
1075
|
+
flags.push('\\NonExistent');
|
|
1076
|
+
}
|
|
1077
|
+
if (options.subscribed) {
|
|
1078
|
+
flags.push('\\Subscribed');
|
|
1079
|
+
}
|
|
1080
|
+
flags.push(...(options.extra || []));
|
|
1081
|
+
// \Noinferiors implies \HasNoChildren (RFC 5258 section 3.4 and section 4 example 3)
|
|
1082
|
+
if (flags.indexOf('\\Noinferiors') < 0) {
|
|
1083
|
+
flags.push(options.hasChildren ? '\\HasChildren' : '\\HasNoChildren');
|
|
1084
|
+
}
|
|
1085
|
+
return flags;
|
|
1086
|
+
}
|
|
1087
|
+
matchFolders(referenceName, match, exportName, folders) {
|
|
1088
|
+
let includeINBOX = false;
|
|
1089
|
+
const source = folders || this.folderCache;
|
|
1090
|
+
const toName = exportName || ((name) => name);
|
|
1091
|
+
let reference = referenceName || '';
|
|
1092
|
+
if (reference === '' && this.referenceNamespace !== false) {
|
|
1093
|
+
reference = toName(this.referenceNamespace);
|
|
1094
|
+
includeINBOX = true;
|
|
1095
|
+
}
|
|
1096
|
+
// the reference does not have to be a namespace, use the namespace it belongs to
|
|
1097
|
+
let nsKey = false;
|
|
1098
|
+
let nsName = '';
|
|
1099
|
+
for (const key of Object.keys(this.storage)) {
|
|
1100
|
+
const name = toName(key);
|
|
1101
|
+
if (key !== 'INBOX' && reference.substr(0, name.length) === name && (nsKey === false || name.length > nsName.length)) {
|
|
1102
|
+
nsKey = key;
|
|
1103
|
+
nsName = name;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
if (nsKey === false) {
|
|
1107
|
+
return [];
|
|
1108
|
+
}
|
|
1109
|
+
const namespace = this.storage[nsKey];
|
|
1110
|
+
const lookup = reference + match;
|
|
1111
|
+
const result = [];
|
|
1112
|
+
const pattern = '^' +
|
|
1113
|
+
lookup
|
|
1114
|
+
// escape regex symbols
|
|
1115
|
+
.replace(/([\\^$+?!.():=[\]{}|,-])/g, '\\$1')
|
|
1116
|
+
.replace(/[*]/g, '.*')
|
|
1117
|
+
.replace(/[%]/g, '[^' + namespace.separator.replace(/([\\^$+*?!.():=[\]{}|,-])/g, '\\$1') + ']*') +
|
|
1118
|
+
'$';
|
|
1119
|
+
const query = new RegExp(pattern, '');
|
|
1120
|
+
// INBOX is case-insensitive
|
|
1121
|
+
if (includeINBOX && source.INBOX && ((reference ? reference + namespace.separator : '') + 'INBOX').match(new RegExp(pattern, 'i'))) {
|
|
1122
|
+
result.push(source.INBOX);
|
|
1123
|
+
}
|
|
1124
|
+
Object.keys(source).forEach(path => {
|
|
1125
|
+
const folder = source[path];
|
|
1126
|
+
if (folder.namespace !== nsKey) {
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
const name = toName(path);
|
|
1130
|
+
if (name.match(query) && (folder.flags.indexOf('\\NonExistent') < 0 || name === match)) {
|
|
1131
|
+
result.push(folder);
|
|
1132
|
+
}
|
|
1133
|
+
});
|
|
1134
|
+
return result;
|
|
1135
|
+
}
|
|
1136
|
+
/**
|
|
1137
|
+
* Returns the subscribed names with their superior hierarchy levels, for LSUB and LIST (SUBSCRIBED).
|
|
1138
|
+
* Names that are not mailboxes get a stand-in object with \Noselect, which LIST-EXTENDED reports as
|
|
1139
|
+
* \NonExistent (RFC 5258 section 3), and `subscribed` false for a level that is only listed because
|
|
1140
|
+
* of a subscribed name below it
|
|
1141
|
+
*
|
|
1142
|
+
* @return {Object} path to mailbox object or stand-in, usable as the `folders` of matchFolders
|
|
1143
|
+
*/
|
|
1144
|
+
getSubscriptionTree() {
|
|
1145
|
+
const tree = Object.create(null);
|
|
1146
|
+
const add = (path, subscribed) => {
|
|
1147
|
+
tree[path] = tree[path] || this.getMailbox(path) || { path, namespace: this.getNamespace(path), flags: ['\\Noselect'], subscribed };
|
|
1148
|
+
};
|
|
1149
|
+
const names = [...this.subscriptions].filter(path => path === 'INBOX' || this.getNamespace(path) !== false);
|
|
1150
|
+
names.forEach(path => add(path, true));
|
|
1151
|
+
names.forEach(path => {
|
|
1152
|
+
// superior levels of the name within its namespace
|
|
1153
|
+
for (let parent = this.getParentPath(path); parent; parent = this.getParentPath(parent)) {
|
|
1154
|
+
add(parent, false);
|
|
1155
|
+
}
|
|
1156
|
+
});
|
|
1157
|
+
return tree;
|
|
1158
|
+
}
|
|
1159
|
+
/**
|
|
1160
|
+
* Retrieves an array of messages that fit in the specified range criteria
|
|
1161
|
+
*
|
|
1162
|
+
* @param {Object|String} mailbox Mailbox to look for the messages
|
|
1163
|
+
* @param {String} range Message range (eg. "*:4,5,7:9")
|
|
1164
|
+
* @param {Boolean} isUid If true, use UID values, not sequence indexes for comparison
|
|
1165
|
+
* @return {Array} An array of messages in the form of [[seqIndex, message]]
|
|
1166
|
+
*/
|
|
1167
|
+
getMessageRange(source, sequence, isUid) {
|
|
1168
|
+
const range = (sequence || '').toString();
|
|
1169
|
+
const mailbox = typeof source === 'string' ? this.getMailbox(source) : source;
|
|
1170
|
+
// sequence-set from RFC 3501 and RFC 9051 section 9, numbers are nz-number values (32-bit), UIDs too
|
|
1171
|
+
if (!isSequenceSet(range)) {
|
|
1172
|
+
const err = new Error('Invalid sequence set');
|
|
1173
|
+
err.imapResponse = 'BAD';
|
|
1174
|
+
throw err;
|
|
1175
|
+
}
|
|
1176
|
+
const result = [];
|
|
1177
|
+
const rangeParts = range.split(',');
|
|
1178
|
+
const messages = Array.isArray(mailbox) ? mailbox : mailbox.messages;
|
|
1179
|
+
let uid;
|
|
1180
|
+
const totalMessages = messages.length;
|
|
1181
|
+
let maxUid = 0;
|
|
1182
|
+
const inRange = function (nr, ranges, total) {
|
|
1183
|
+
for (let i = 0, len = ranges.length; i < len; i++) {
|
|
1184
|
+
const to = ranges[i].split(':');
|
|
1185
|
+
const first = to.shift();
|
|
1186
|
+
const from = Number(first === '*' ? total : first) || 1;
|
|
1187
|
+
const last = to.pop() || from;
|
|
1188
|
+
const end = Number((last === '*' && total) || last) || from;
|
|
1189
|
+
if (nr >= Math.min(from, end) && nr <= Math.max(from, end)) {
|
|
1190
|
+
return true;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
return false;
|
|
1194
|
+
};
|
|
1195
|
+
messages.forEach(message => {
|
|
1196
|
+
if (message.uid > maxUid) {
|
|
1197
|
+
maxUid = message.uid;
|
|
1198
|
+
}
|
|
1199
|
+
});
|
|
1200
|
+
for (let i = 0, len = messages.length; i < len; i++) {
|
|
1201
|
+
uid = messages[i].uid || 1;
|
|
1202
|
+
if (inRange(isUid ? uid : i + 1, rangeParts, isUid ? maxUid : totalMessages)) {
|
|
1203
|
+
result.push([i + 1, messages[i]]);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
return result;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
class IMAPConnection {
|
|
1210
|
+
constructor(server, socket) {
|
|
1211
|
+
this.server = server;
|
|
1212
|
+
this.socket = socket;
|
|
1213
|
+
this.options = this.server.options;
|
|
1214
|
+
this.state = 'Not Authenticated';
|
|
1215
|
+
this.secureConnection = !!this.options.secureConnection;
|
|
1216
|
+
this._remainder = '';
|
|
1217
|
+
this._command = '';
|
|
1218
|
+
this._literalRemaining = 0;
|
|
1219
|
+
this.inputHandler = false;
|
|
1220
|
+
// a layer between the socket and the IMAP protocol, such as COMPRESS=DEFLATE (RFC 4978). It has
|
|
1221
|
+
// the methods write(buffer), receive(chunk), end(callback) and destroy(), and passes data on with
|
|
1222
|
+
// connection.writeRaw() and connection.onData(), so it always sits above TLS (RFC 4978 section 3)
|
|
1223
|
+
this.transport = null;
|
|
1224
|
+
// per connection options for the imap-handler parser and compiler, plugins can change these. The
|
|
1225
|
+
// parser options go on top of server.parserOptions
|
|
1226
|
+
this.parserOptions = {};
|
|
1227
|
+
this.compilerOptions = {};
|
|
1228
|
+
// message/global parts encapsulate a message like message/rfc822 parts in BODYSTRUCTURE and in section
|
|
1229
|
+
// numbers. IMAP4rev2 sets it (RFC 9051 sections 6.4.5.1 and 7.5.2), IMAP4rev1 describes them as basic parts
|
|
1230
|
+
this.messageGlobal = false;
|
|
1231
|
+
this._commandQueue = [];
|
|
1232
|
+
this._processing = false;
|
|
1233
|
+
if (this.options.debug) {
|
|
1234
|
+
this.socket.pipe(process.stdout);
|
|
1235
|
+
}
|
|
1236
|
+
this.socket.on('data', this.receive.bind(this));
|
|
1237
|
+
this.socket.on('close', this.onClose.bind(this));
|
|
1238
|
+
this.socket.on('error', this.onError.bind(this));
|
|
1239
|
+
this.directNotifications = false;
|
|
1240
|
+
this._notificationCallback = this.onNotify.bind(this);
|
|
1241
|
+
this.notificationQueue = [];
|
|
1242
|
+
this.server.on('notify', this._notificationCallback);
|
|
1243
|
+
this.server.connections.add(this);
|
|
1244
|
+
this.write('* OK ImapKit ready for rumble\r\n');
|
|
1245
|
+
}
|
|
1246
|
+
/**
|
|
1247
|
+
* Writes protocol output to the client, through the transport layer if there is one
|
|
1248
|
+
*
|
|
1249
|
+
* @param {Buffer|String} data Data to send, a string is sent as a binary string
|
|
1250
|
+
*/
|
|
1251
|
+
write(data) {
|
|
1252
|
+
const buffer = typeof data === 'string' ? Buffer.from(data, 'binary') : data;
|
|
1253
|
+
if (this.transport) {
|
|
1254
|
+
this.transport.write(buffer);
|
|
1255
|
+
}
|
|
1256
|
+
else {
|
|
1257
|
+
this.writeRaw(buffer);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* Writes data to the socket, below the transport layer
|
|
1262
|
+
*
|
|
1263
|
+
* @param {Buffer} data Data to send
|
|
1264
|
+
*/
|
|
1265
|
+
writeRaw(data) {
|
|
1266
|
+
if (this.socket && !this.socket.destroyed) {
|
|
1267
|
+
this.socket.write(data);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* Handles data from the socket, through the transport layer if there is one
|
|
1272
|
+
*
|
|
1273
|
+
* @param {Buffer} chunk Received data
|
|
1274
|
+
*/
|
|
1275
|
+
receive(chunk) {
|
|
1276
|
+
if (this.transport) {
|
|
1277
|
+
this.transport.receive(chunk);
|
|
1278
|
+
}
|
|
1279
|
+
else {
|
|
1280
|
+
this.onData(chunk);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
/**
|
|
1284
|
+
* Closes the connection once everything sent so far, including data a transport layer still
|
|
1285
|
+
* holds, is written out
|
|
1286
|
+
*/
|
|
1287
|
+
end() {
|
|
1288
|
+
const socket = this.socket;
|
|
1289
|
+
if (!socket) {
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
this._closing = true;
|
|
1293
|
+
if (this.transport) {
|
|
1294
|
+
this.transport.end(() => socket.end());
|
|
1295
|
+
}
|
|
1296
|
+
else {
|
|
1297
|
+
socket.end();
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
/**
|
|
1301
|
+
* Sends an untagged BYE, drops unprocessed input and closes the connection (RFC 3501 section 7.1.5)
|
|
1302
|
+
*
|
|
1303
|
+
* @param {String} text Human readable explanation
|
|
1304
|
+
* @param {String} [description] Description for output handlers
|
|
1305
|
+
*/
|
|
1306
|
+
bye(text, description) {
|
|
1307
|
+
this.sendStatus({ tag: '*' }, null, 'BYE', text, false, description || 'BYE');
|
|
1308
|
+
// the selected mailbox is left without expunging, queued notifications are dropped
|
|
1309
|
+
this.closeMailbox();
|
|
1310
|
+
this.state = 'Logout';
|
|
1311
|
+
this.discardInput();
|
|
1312
|
+
this.end();
|
|
1313
|
+
}
|
|
1314
|
+
/**
|
|
1315
|
+
* Checks if the client sent anything after the command that is running, that is not processed yet
|
|
1316
|
+
*
|
|
1317
|
+
* @return {Boolean} true if there is unprocessed input or a queued command
|
|
1318
|
+
*/
|
|
1319
|
+
hasPendingInput() {
|
|
1320
|
+
return !!(this._remainder || this._command || this._literalRemaining || this._commandQueue.length);
|
|
1321
|
+
}
|
|
1322
|
+
/**
|
|
1323
|
+
* Checks if a command is running or waiting, for checks that must not run ahead of earlier commands
|
|
1324
|
+
*
|
|
1325
|
+
* @return {Boolean} true if a command is running or queued
|
|
1326
|
+
*/
|
|
1327
|
+
isBusy() {
|
|
1328
|
+
return !!(this._processing || this._commandQueue.length);
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* Checks if a command was sent together with a command that was refused for the noPipelining option (STARTTLS,
|
|
1332
|
+
* COMPRESS): it arrived in the same read, before the client could see the refusal
|
|
1333
|
+
*
|
|
1334
|
+
* @return {Boolean} true if the command must be refused
|
|
1335
|
+
*/
|
|
1336
|
+
isPipelinedAfterRefusal() {
|
|
1337
|
+
return !!this._pipelinedAfter && this._pipelinedAfter.read === this._readCount;
|
|
1338
|
+
}
|
|
1339
|
+
/**
|
|
1340
|
+
* Refuses a command that was pipelined after a refused noPipelining command, without running it
|
|
1341
|
+
*
|
|
1342
|
+
* @param {Object} parsed Parsed command
|
|
1343
|
+
* @param {String} data Raw command
|
|
1344
|
+
*/
|
|
1345
|
+
refusePipelined(parsed, data) {
|
|
1346
|
+
this.sendStatus(parsed, data, 'BAD', 'Commands must not be pipelined after ' + this._pipelinedAfter?.command, false, 'INVALID COMMAND');
|
|
1347
|
+
}
|
|
1348
|
+
/**
|
|
1349
|
+
* Drops input that is not processed yet, including queued commands
|
|
1350
|
+
*/
|
|
1351
|
+
discardInput() {
|
|
1352
|
+
this._commandQueue = [];
|
|
1353
|
+
this._remainder = '';
|
|
1354
|
+
this._command = '';
|
|
1355
|
+
this._literalRemaining = 0;
|
|
1356
|
+
this._skipCommand = false;
|
|
1357
|
+
this._earlyLiteral = false;
|
|
1358
|
+
}
|
|
1359
|
+
/**
|
|
1360
|
+
* Returns the connection to the Not Authenticated state and resets everything but the TLS
|
|
1361
|
+
* layer (RFC 8437 section 3): the selected mailbox is closed without EXPUNGE responses, and the
|
|
1362
|
+
* plugins clear their session state (ENABLEd extensions, CONDSTORE, COMPRESS, ...) with
|
|
1363
|
+
* server.resetHandlers. Call it after the response that ends the session was sent.
|
|
1364
|
+
*/
|
|
1365
|
+
resetSession() {
|
|
1366
|
+
this.closeMailbox();
|
|
1367
|
+
this.state = 'Not Authenticated';
|
|
1368
|
+
this.username = false;
|
|
1369
|
+
this.everSelected = false;
|
|
1370
|
+
this.directNotifications = false;
|
|
1371
|
+
this.server.resetHandlers.forEach(handler => handler(this));
|
|
1372
|
+
}
|
|
1373
|
+
/**
|
|
1374
|
+
* Closes the selected mailbox, if there is one, and returns to the Authenticated state (CLOSE, UNSELECT, a failed
|
|
1375
|
+
* SELECT or EXAMINE, RFC 3501 section 6.3.1). The read-only mode, the \Recent set of the session and the
|
|
1376
|
+
* notifications that were not sent yet are dropped. Sends nothing, the caller answers the command
|
|
1377
|
+
*/
|
|
1378
|
+
closeMailbox() {
|
|
1379
|
+
this.state = 'Authenticated';
|
|
1380
|
+
this.selectedMailbox = false;
|
|
1381
|
+
this.readOnly = false;
|
|
1382
|
+
this.recent = null;
|
|
1383
|
+
this.notificationQueue = [];
|
|
1384
|
+
}
|
|
1385
|
+
onClose() {
|
|
1386
|
+
if (this.socket) {
|
|
1387
|
+
this.socket.removeAllListeners();
|
|
1388
|
+
this.socket = null;
|
|
1389
|
+
}
|
|
1390
|
+
if (this.transport) {
|
|
1391
|
+
this.transport.destroy();
|
|
1392
|
+
this.transport = null;
|
|
1393
|
+
}
|
|
1394
|
+
this.server.removeListener('notify', this._notificationCallback);
|
|
1395
|
+
this.server.connections.delete(this);
|
|
1396
|
+
}
|
|
1397
|
+
onError(err) {
|
|
1398
|
+
if (this.options.debug) {
|
|
1399
|
+
console.log('Socket error event emitted, %s', Date());
|
|
1400
|
+
console.log(err.stack);
|
|
1401
|
+
}
|
|
1402
|
+
try {
|
|
1403
|
+
this.socket?.end();
|
|
1404
|
+
}
|
|
1405
|
+
catch (E) {
|
|
1406
|
+
// socket is already gone
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
onData(chunk) {
|
|
1410
|
+
let match;
|
|
1411
|
+
let str;
|
|
1412
|
+
// everything in one read arrived before anything this read causes to be sent
|
|
1413
|
+
this._readCount = (this._readCount || 0) + 1;
|
|
1414
|
+
str = (chunk || '').toString('binary');
|
|
1415
|
+
if (this._discardLine) {
|
|
1416
|
+
// skipping the rest of a command line that was too long
|
|
1417
|
+
const lineEnd = str.indexOf('\n');
|
|
1418
|
+
if (lineEnd < 0) {
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
// the command was not executed, but its tag still gets an answer
|
|
1422
|
+
const tag = this._discardLine;
|
|
1423
|
+
this._discardLine = false;
|
|
1424
|
+
str = str.substr(lineEnd + 1);
|
|
1425
|
+
this.sendBad(tag, 'Command line too long', 'LINE TOO LONG');
|
|
1426
|
+
}
|
|
1427
|
+
if (this._literalRemaining) {
|
|
1428
|
+
str = this.readLiteral(str);
|
|
1429
|
+
if (this._literalRemaining) {
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
// non-synchronizing literals are only valid when LITERAL+ or LITERAL- is advertised. A literal8 marker `~{n}`
|
|
1434
|
+
// (RFC 3516) is matched always, so it is refused when BINARY is not loaded
|
|
1435
|
+
const lineEndRegex = this.server.literalPlus
|
|
1436
|
+
? /(?<marker>(?<tilde>~)?\{(?<size>\d+)(?<plus>\+)?\})?(?<cr>\r?)\n/
|
|
1437
|
+
: /(?<marker>(?<tilde>~)?\{(?<size>\d+)\})?(?<cr>\r?)\n/;
|
|
1438
|
+
this._remainder = str = this._remainder + str;
|
|
1439
|
+
while ((match = lineEndRegex.exec(str))) {
|
|
1440
|
+
const { marker, tilde, size, plus, cr } = match.groups;
|
|
1441
|
+
if (match.index > MAX_LINE_LENGTH) {
|
|
1442
|
+
// the same limit as for a line that arrives in several reads (see the end of this method), so
|
|
1443
|
+
// the outcome does not depend on how the input was split into reads (RFC 3501 section 7.1.3)
|
|
1444
|
+
const tag = getResponseTag(this._command || str);
|
|
1445
|
+
this.sendBad('*', 'Command line too long', 'LINE TOO LONG');
|
|
1446
|
+
this.sendBad(tag, 'Command line too long', 'LINE TOO LONG');
|
|
1447
|
+
this._remainder = str = str.substr(match.index + match[0].length);
|
|
1448
|
+
this._command = '';
|
|
1449
|
+
this._skipCommand = false;
|
|
1450
|
+
continue;
|
|
1451
|
+
}
|
|
1452
|
+
if (!cr) {
|
|
1453
|
+
// every command line ends with CRLF (RFC 3501 section 9), a bare LF is refused
|
|
1454
|
+
const line = this._command + str.substr(0, match.index + match[0].length - 1);
|
|
1455
|
+
this._remainder = str = str.substr(match.index + match[0].length);
|
|
1456
|
+
this._command = '';
|
|
1457
|
+
if (this._skipCommand) {
|
|
1458
|
+
this._skipCommand = false;
|
|
1459
|
+
}
|
|
1460
|
+
else {
|
|
1461
|
+
this.sendBad(this.inputHandler ? '*' : getResponseTag(line), 'Lines must end with CRLF', 'INVALID LINE ENDING', line);
|
|
1462
|
+
}
|
|
1463
|
+
continue;
|
|
1464
|
+
}
|
|
1465
|
+
if (!size || (this._skipCommand && !plus)) {
|
|
1466
|
+
// the command is complete, or it was refused already and the client waits in vain
|
|
1467
|
+
// for a continuation request
|
|
1468
|
+
const line = this._command + str.substr(0, match.index);
|
|
1469
|
+
this._remainder = str.substr(match.index + match[0].length);
|
|
1470
|
+
this._command = '';
|
|
1471
|
+
if (this._skipCommand) {
|
|
1472
|
+
this._skipCommand = false;
|
|
1473
|
+
}
|
|
1474
|
+
else if (this._earlyLiteral) {
|
|
1475
|
+
// the client sent literal data without waiting for the continuation request
|
|
1476
|
+
this._earlyLiteral = false;
|
|
1477
|
+
this.sendBad(getResponseTag(line), 'Literal data must wait for the continuation request', 'LITERAL TOO EARLY', line);
|
|
1478
|
+
}
|
|
1479
|
+
else if (this.inputHandler) {
|
|
1480
|
+
this.inputHandler(line);
|
|
1481
|
+
}
|
|
1482
|
+
else {
|
|
1483
|
+
this.scheduleCommand(line);
|
|
1484
|
+
}
|
|
1485
|
+
if (this.upgrading) {
|
|
1486
|
+
// STARTTLS was accepted, ignore any pipelined plaintext input
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
// a handler may have dropped the input that followed with discardInput()
|
|
1490
|
+
str = this._remainder;
|
|
1491
|
+
continue;
|
|
1492
|
+
}
|
|
1493
|
+
const literalSize = Number(size);
|
|
1494
|
+
if (!this._skipCommand) {
|
|
1495
|
+
const line = this._command + str.substr(0, match.index);
|
|
1496
|
+
// the literal marker is part of the first word when it directly follows the tag
|
|
1497
|
+
const tag = getResponseTag(line + marker);
|
|
1498
|
+
const refusal = this.checkLiteral(line, literalSize, !plus, !!tilde);
|
|
1499
|
+
if (refusal) {
|
|
1500
|
+
if (plus) {
|
|
1501
|
+
// the client is going to send the literal anyway, so there is no way to recover. A BYE
|
|
1502
|
+
// for a literal that is too large should carry TOOBIG (RFC 7888 section 5)
|
|
1503
|
+
this.sendStatus({ tag: '*' }, line, 'BYE', refusal.text, refusal.text === LITERAL_TOO_LARGE && 'TOOBIG', 'LITERAL REFUSED');
|
|
1504
|
+
this._remainder = this._command = '';
|
|
1505
|
+
this.end();
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1508
|
+
// refuse a synchronizing literal by not sending a continuation request
|
|
1509
|
+
this.sendStatus({ tag }, line, refusal.command, refusal.text, refusal.code || false, 'LITERAL REFUSED');
|
|
1510
|
+
this._remainder = str = str.substr(match.index + match[0].length);
|
|
1511
|
+
this._command = '';
|
|
1512
|
+
continue;
|
|
1513
|
+
}
|
|
1514
|
+
if (plus && literalSize > this.server.nonSyncLiteralLimit) {
|
|
1515
|
+
// RFC 7888 sections 4 and 5: the command is refused with TOOBIG, the literal and the
|
|
1516
|
+
// rest of the command are read and dropped
|
|
1517
|
+
this.sendStatus({ tag }, line, 'BAD', 'Non-synchronizing literals are limited to ' + this.server.nonSyncLiteralLimit + ' octets', 'TOOBIG', 'LITERAL TOO BIG');
|
|
1518
|
+
this._skipCommand = true;
|
|
1519
|
+
this._command = '';
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
if (!plus) {
|
|
1523
|
+
if (str.length > match.index + match[0].length) {
|
|
1524
|
+
// RFC 3501 section 4.3: the client MUST wait for the continuation request
|
|
1525
|
+
// before sending the octets of a synchronizing literal
|
|
1526
|
+
this._earlyLiteral = true;
|
|
1527
|
+
}
|
|
1528
|
+
else if (!this._earlyLiteral) {
|
|
1529
|
+
this.write('+ Go ahead\r\n');
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
this._remainder = '';
|
|
1533
|
+
if (!this._skipCommand) {
|
|
1534
|
+
this._command += str.substr(0, match.index + match[0].length);
|
|
1535
|
+
}
|
|
1536
|
+
this._literalRemaining = literalSize;
|
|
1537
|
+
str = this.readLiteral(str.substr(match.index + match[0].length));
|
|
1538
|
+
if (this._literalRemaining) {
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
this._remainder = str;
|
|
1542
|
+
}
|
|
1543
|
+
if (this._remainder.length > MAX_LINE_LENGTH) {
|
|
1544
|
+
// RFC 3501 section 7.1.3
|
|
1545
|
+
this.sendBad('*', 'Command line too long', 'LINE TOO LONG');
|
|
1546
|
+
const tag = getResponseTag(this._command || this._remainder);
|
|
1547
|
+
this._remainder = '';
|
|
1548
|
+
this._command = '';
|
|
1549
|
+
this._skipCommand = false;
|
|
1550
|
+
this._discardLine = tag;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
/**
|
|
1554
|
+
* Reads literal data that the current command is waiting for. The data of a command that was
|
|
1555
|
+
* refused is dropped.
|
|
1556
|
+
*
|
|
1557
|
+
* @param {String} str Received data
|
|
1558
|
+
* @return {String} the data that follows the literal
|
|
1559
|
+
*/
|
|
1560
|
+
readLiteral(str) {
|
|
1561
|
+
const length = Math.min(this._literalRemaining, str.length);
|
|
1562
|
+
if (!this._skipCommand) {
|
|
1563
|
+
this._command += str.substr(0, length);
|
|
1564
|
+
}
|
|
1565
|
+
this._literalRemaining -= length;
|
|
1566
|
+
return str.substr(length);
|
|
1567
|
+
}
|
|
1568
|
+
/**
|
|
1569
|
+
* Sends a BAD response to input that did not make it to a command handler
|
|
1570
|
+
*
|
|
1571
|
+
* @param {String} tag Tag to answer with, "*" for an untagged response
|
|
1572
|
+
* @param {String} text Human readable text
|
|
1573
|
+
* @param {String} description Description for output handlers
|
|
1574
|
+
* @param {String} [data] Raw input
|
|
1575
|
+
*/
|
|
1576
|
+
sendBad(tag, text, description, data) {
|
|
1577
|
+
this.sendStatus({ tag }, data, 'BAD', text, false, description);
|
|
1578
|
+
}
|
|
1579
|
+
/**
|
|
1580
|
+
* Checks if a literal may be accepted for the command line received so far. Literals are
|
|
1581
|
+
* refused before they are read when they are too large, or when the command is unknown or
|
|
1582
|
+
* not allowed in the current state, so the client does not get a continuation request for
|
|
1583
|
+
* a command that is going to fail anyway.
|
|
1584
|
+
*
|
|
1585
|
+
* @param {String} line The command received so far (with earlier literals), up to the literal size marker
|
|
1586
|
+
* @param {Number} literalSize Size of the literal in octets
|
|
1587
|
+
* @param {Boolean} [synchronizing] true if the literal can still be refused without reading it
|
|
1588
|
+
* @param {Boolean} [literal8] The literal is a literal8 `~{n}`
|
|
1589
|
+
* @return {Object|Boolean} `{ command, code, text }` for the response that refuses the literal, or false.
|
|
1590
|
+
* Responses from `server.literalFilters` have the same form
|
|
1591
|
+
*/
|
|
1592
|
+
checkLiteral(line, literalSize, synchronizing, literal8) {
|
|
1593
|
+
const refuse = (text) => ({ command: 'BAD', text });
|
|
1594
|
+
const maxLiteralSize = this.getMaxLiteralSize();
|
|
1595
|
+
if (literalSize > maxLiteralSize || line.length + literalSize > maxLiteralSize + MAX_LINE_LENGTH) {
|
|
1596
|
+
return refuse(LITERAL_TOO_LARGE);
|
|
1597
|
+
}
|
|
1598
|
+
if (this.isPipelinedAfterRefusal()) {
|
|
1599
|
+
return refuse('Commands must not be pipelined after ' + this._pipelinedAfter?.command);
|
|
1600
|
+
}
|
|
1601
|
+
if (this.inputHandler) {
|
|
1602
|
+
// not a command, e.g. a SASL response
|
|
1603
|
+
return literal8 ? refuse('Literal8 is not allowed here') : false;
|
|
1604
|
+
}
|
|
1605
|
+
// tag SP command, and for UID and AUTHENTICATE the word that follows
|
|
1606
|
+
const words = line.match(/^[^ ]* ([^ ]*)(?: ([^ ]*))?/);
|
|
1607
|
+
let command = ((words && words[1]) || '').toUpperCase();
|
|
1608
|
+
if (command === 'UID' || command === 'AUTHENTICATE') {
|
|
1609
|
+
command += ' ' + ((words && words[2]) || '').toUpperCase();
|
|
1610
|
+
}
|
|
1611
|
+
if (!COMMAND_REGEX.test(command) || !this.server.getCommandHandler(command)) {
|
|
1612
|
+
return refuse('Unknown command');
|
|
1613
|
+
}
|
|
1614
|
+
const options = this.server.getCommandOptions(command);
|
|
1615
|
+
if (options.states && options.states.indexOf(this.state) < 0) {
|
|
1616
|
+
return refuse(stateError(command, this.state));
|
|
1617
|
+
}
|
|
1618
|
+
// RFC 3516 section 7: literal8 is only valid where an extension allows it, like BINARY for the APPEND
|
|
1619
|
+
// message or METADATA for entry values (RFC 5464 section 5)
|
|
1620
|
+
if (literal8 && !(options.literal8 === true || (options.literal8 && Object.hasOwn(this.server.capabilities, options.literal8)))) {
|
|
1621
|
+
return refuse('Literal8 is not allowed in ' + command);
|
|
1622
|
+
}
|
|
1623
|
+
// plugins can refuse a synchronizing literal, e.g. a message that is too large for APPEND.
|
|
1624
|
+
// A non-synchronizing literal is read anyway, the command handler refuses it later
|
|
1625
|
+
if (synchronizing) {
|
|
1626
|
+
for (const filter of this.server.literalFilters) {
|
|
1627
|
+
const refusal = filter(this, command, line, literalSize);
|
|
1628
|
+
if (refusal) {
|
|
1629
|
+
return refusal;
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
return false;
|
|
1634
|
+
}
|
|
1635
|
+
/**
|
|
1636
|
+
* Returns the largest literal the client may send in its current state. Before
|
|
1637
|
+
* authentication only small literals (user names, passwords) make sense.
|
|
1638
|
+
*
|
|
1639
|
+
* @return {Number} Size in bytes
|
|
1640
|
+
*/
|
|
1641
|
+
getMaxLiteralSize() {
|
|
1642
|
+
if (this.state === 'Not Authenticated') {
|
|
1643
|
+
return MAX_PREAUTH_LITERAL_SIZE;
|
|
1644
|
+
}
|
|
1645
|
+
return Number(this.options.maxLiteralSize) || MAX_LITERAL_SIZE;
|
|
1646
|
+
}
|
|
1647
|
+
/**
|
|
1648
|
+
* Returns the message list of the selected mailbox as this session currently
|
|
1649
|
+
* sees it. When another session has expunged messages that this session has
|
|
1650
|
+
* not been told about yet, sequence numbers must still refer to the old list.
|
|
1651
|
+
*
|
|
1652
|
+
* @return {Array} List of messages
|
|
1653
|
+
*/
|
|
1654
|
+
getSessionMessages() {
|
|
1655
|
+
for (let i = 0, len = this.notificationQueue.length; i < len; i++) {
|
|
1656
|
+
const mailboxCopy = this.notificationQueue[i].mailboxCopy;
|
|
1657
|
+
if (mailboxCopy) {
|
|
1658
|
+
return mailboxCopy;
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
return this.selectedMailbox ? this.selectedMailbox.messages : [];
|
|
1662
|
+
}
|
|
1663
|
+
/**
|
|
1664
|
+
* Resolves the sequence set argument of a command to messages of the selected mailbox, as this
|
|
1665
|
+
* session sees it. Plugins can replace it per connection to support other forms of sequence sets
|
|
1666
|
+
* (e.g. "$" of SEARCHRES)
|
|
1667
|
+
*
|
|
1668
|
+
* @param {String} range Sequence set
|
|
1669
|
+
* @param {Boolean} isUid If true, the set lists UIDs instead of sequence numbers
|
|
1670
|
+
* @return {Array} An array of messages in the form of [[seqIndex, message]]
|
|
1671
|
+
*/
|
|
1672
|
+
getMessageRange(range, isUid) {
|
|
1673
|
+
return this.server.getMessageRange(this.getSessionMessages(), range, isUid);
|
|
1674
|
+
}
|
|
1675
|
+
/**
|
|
1676
|
+
* Lets `server.rangeLimits` cut the messages a command operates on, after its sequence set argument was resolved.
|
|
1677
|
+
* A limit that returns the messages from the highest UID down sets `parsed.highestFirst`, then MOVE and UID EXPUNGE
|
|
1678
|
+
* send their EXPUNGE responses in that order too
|
|
1679
|
+
*
|
|
1680
|
+
* @param {Object} parsed Parsed command
|
|
1681
|
+
* @param {Array} range Messages of the sequence set, in the form of [[seqIndex, message]]
|
|
1682
|
+
* @return {Array} the messages to operate on, in the same form
|
|
1683
|
+
*/
|
|
1684
|
+
limitRange(parsed, range) {
|
|
1685
|
+
return this.server.rangeLimits.reduce((result, limit) => limit(this, parsed, result) || result, range);
|
|
1686
|
+
}
|
|
1687
|
+
/**
|
|
1688
|
+
* Resolves the sequence set argument of a command like getMessageRange, but refuses message sequence
|
|
1689
|
+
* numbers past the end of the selected mailbox, as this session sees it. RFC 3501 and RFC 9051 section 9
|
|
1690
|
+
* (seq-number): "The server should respond with a tagged BAD response to a command that uses a message
|
|
1691
|
+
* sequence number greater than the number of messages in the selected mailbox. This includes "*" if the
|
|
1692
|
+
* selected mailbox is empty." For the sequence set argument of FETCH, STORE, COPY and MOVE, SEARCH keys
|
|
1693
|
+
* use getMessageRange
|
|
1694
|
+
*
|
|
1695
|
+
* @param {String} range Sequence set
|
|
1696
|
+
* @param {Boolean} isUid If true, the set lists UIDs, these can point past the end
|
|
1697
|
+
* @return {Array} An array of messages in the form of [[seqIndex, message]], see getMessageRange
|
|
1698
|
+
* @throws {Error} BAD error if a sequence number is out of range
|
|
1699
|
+
*/
|
|
1700
|
+
getCommandRange(range, isUid) {
|
|
1701
|
+
const result = this.getMessageRange(range, isUid);
|
|
1702
|
+
if (isUid) {
|
|
1703
|
+
return result;
|
|
1704
|
+
}
|
|
1705
|
+
const total = this.getSessionMessages().length;
|
|
1706
|
+
String(range)
|
|
1707
|
+
.split(/[,:]/)
|
|
1708
|
+
.forEach(value => {
|
|
1709
|
+
if (value === '*' ? !total : Number(value) > total) {
|
|
1710
|
+
const err = new Error(total ? 'Message sequence number ' + value + ' is greater than the number of messages (' + total + ')' : 'The mailbox is empty');
|
|
1711
|
+
err.imapResponse = 'BAD';
|
|
1712
|
+
throw err;
|
|
1713
|
+
}
|
|
1714
|
+
});
|
|
1715
|
+
return result;
|
|
1716
|
+
}
|
|
1717
|
+
/**
|
|
1718
|
+
* Checks if this session has EXPUNGE notifications that it has not been told about yet
|
|
1719
|
+
*
|
|
1720
|
+
* @return {Boolean} true if an EXPUNGE response is pending
|
|
1721
|
+
*/
|
|
1722
|
+
hasPendingExpunge() {
|
|
1723
|
+
return this.notificationQueue.some(notification => notification.attributes && (notification.attributes[1] || {}).value === 'EXPUNGE');
|
|
1724
|
+
}
|
|
1725
|
+
/**
|
|
1726
|
+
* Tells the other sessions that have the selected mailbox open about changed flags, they get
|
|
1727
|
+
* an untagged FETCH with the new flags (RFC 3501 section 5.2)
|
|
1728
|
+
*
|
|
1729
|
+
* @param {Array} messages Messages with changed flags
|
|
1730
|
+
*/
|
|
1731
|
+
notifyFlagChanges(messages) {
|
|
1732
|
+
if (messages.length && this.selectedMailbox) {
|
|
1733
|
+
this.server.notify({ tag: '*', flagUpdate: messages }, this.selectedMailbox, this);
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
/**
|
|
1737
|
+
* Sends unsolicited FETCH responses with the flags another session changed. The UID is always
|
|
1738
|
+
* included, RFC 9051 section 6.3.13 requires it for unsolicited FETCH responses and it is valid
|
|
1739
|
+
* in IMAP4rev1 as well.
|
|
1740
|
+
*
|
|
1741
|
+
* @param {Array} messages Messages with changed flags
|
|
1742
|
+
* @param {Map} sequence Message to the sequence number this session knows it by
|
|
1743
|
+
* @param {Set} [reported] Messages already reported, these are skipped and the sent ones are added
|
|
1744
|
+
*/
|
|
1745
|
+
sendFlagUpdate(messages, sequence, reported) {
|
|
1746
|
+
const getFlags = this.server.fetchHandlers.FLAGS || fetchHandlers.FLAGS;
|
|
1747
|
+
messages.forEach(message => {
|
|
1748
|
+
if (!sequence.has(message) || message.ghost) {
|
|
1749
|
+
// the message is gone, its EXPUNGE response tells the rest
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
if (reported) {
|
|
1753
|
+
if (reported.has(message)) {
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1756
|
+
reported.add(message);
|
|
1757
|
+
}
|
|
1758
|
+
this.send({
|
|
1759
|
+
tag: '*',
|
|
1760
|
+
notification: true,
|
|
1761
|
+
attributes: [
|
|
1762
|
+
sequence.get(message),
|
|
1763
|
+
{
|
|
1764
|
+
type: 'ATOM',
|
|
1765
|
+
value: 'FETCH'
|
|
1766
|
+
},
|
|
1767
|
+
[
|
|
1768
|
+
{
|
|
1769
|
+
type: 'ATOM',
|
|
1770
|
+
value: 'UID'
|
|
1771
|
+
},
|
|
1772
|
+
message.uid,
|
|
1773
|
+
{
|
|
1774
|
+
type: 'ATOM',
|
|
1775
|
+
value: 'FLAGS'
|
|
1776
|
+
},
|
|
1777
|
+
getFlags(this, message, { type: 'ATOM', value: 'FLAGS' })
|
|
1778
|
+
]
|
|
1779
|
+
]
|
|
1780
|
+
}, 'FLAG NOTIFICATION', null, null, message);
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
/**
|
|
1784
|
+
* Checks if a message has the \Recent flag in this session
|
|
1785
|
+
*
|
|
1786
|
+
* @param {Object} message Message object
|
|
1787
|
+
* @return {Boolean} true if the message is recent for this session
|
|
1788
|
+
*/
|
|
1789
|
+
isRecent(message) {
|
|
1790
|
+
return !!(this.recent && this.recent.has(message));
|
|
1791
|
+
}
|
|
1792
|
+
/**
|
|
1793
|
+
* Returns the flags of a message as seen by this session, including \Recent
|
|
1794
|
+
*
|
|
1795
|
+
* @param {Object} message Message object
|
|
1796
|
+
* @return {Array} List of flags
|
|
1797
|
+
*/
|
|
1798
|
+
getFlags(message) {
|
|
1799
|
+
return this.isRecent(message) ? message.flags.concat('\\Recent') : message.flags;
|
|
1800
|
+
}
|
|
1801
|
+
/**
|
|
1802
|
+
* Checks if FETCH may set the \Seen flag in the selected mailbox (RFC 3501 section 6.4.5).
|
|
1803
|
+
* Plugins can override it for a connection, e.g. ACL without the "s" right
|
|
1804
|
+
*
|
|
1805
|
+
* @return {Boolean} true if \Seen may be set
|
|
1806
|
+
*/
|
|
1807
|
+
canSetSeen() {
|
|
1808
|
+
return !this.readOnly;
|
|
1809
|
+
}
|
|
1810
|
+
/**
|
|
1811
|
+
* The refusal of a command that would change a mailbox selected read-only (EXAMINE, or SELECT answered with
|
|
1812
|
+
* [READ-ONLY], RFC 3501 sections 6.3.1 and 6.3.2): STORE, EXPUNGE, UID EXPUNGE, MOVE and REPLACE. RFC 5530 section 3:
|
|
1813
|
+
* CLIENTBUG, the client was told that the mailbox is read-only
|
|
1814
|
+
*
|
|
1815
|
+
* @return {Object|Boolean} `{ command, code, text }` if the selected mailbox is read-only, otherwise false
|
|
1816
|
+
*/
|
|
1817
|
+
readOnlyRefusal() {
|
|
1818
|
+
return this.readOnly ? { command: 'NO', code: 'CLIENTBUG', text: 'Mailbox is read-only' } : false;
|
|
1819
|
+
}
|
|
1820
|
+
/**
|
|
1821
|
+
* Sends the refusal of readOnlyRefusal() if the selected mailbox is read-only
|
|
1822
|
+
*
|
|
1823
|
+
* @param {Object} parsed Parsed command
|
|
1824
|
+
* @param {String} data Raw command
|
|
1825
|
+
* @param {String} description Description for output handlers
|
|
1826
|
+
* @return {Boolean} true if the command was refused
|
|
1827
|
+
*/
|
|
1828
|
+
refuseReadOnly(parsed, data, description) {
|
|
1829
|
+
const refusal = this.readOnlyRefusal();
|
|
1830
|
+
if (refusal) {
|
|
1831
|
+
this.sendStatus(parsed, data, refusal.command, refusal.text, refusal.code, description);
|
|
1832
|
+
}
|
|
1833
|
+
return !!refusal;
|
|
1834
|
+
}
|
|
1835
|
+
/**
|
|
1836
|
+
* Checks if CLOSE may expunge the selected mailbox (RFC 3501 section 6.4.2). Plugins can
|
|
1837
|
+
* override it for a connection, e.g. ACL without the "e" right
|
|
1838
|
+
*
|
|
1839
|
+
* @return {Boolean} true if messages may be expunged
|
|
1840
|
+
*/
|
|
1841
|
+
canExpunge() {
|
|
1842
|
+
return !this.readOnly;
|
|
1843
|
+
}
|
|
1844
|
+
onNotify(notification) {
|
|
1845
|
+
if (notification.ignoreConnection === this ||
|
|
1846
|
+
(notification.filter && !notification.filter(this)) ||
|
|
1847
|
+
!this.server.notifyFilters.every(filter => filter(this, notification))) {
|
|
1848
|
+
return;
|
|
1849
|
+
}
|
|
1850
|
+
const mailbox = typeof notification.mailbox === 'string' ? this.server.getMailbox(notification.mailbox) : notification.mailbox;
|
|
1851
|
+
if (!notification.mailbox || (this.selectedMailbox && this.selectedMailbox === mailbox)) {
|
|
1852
|
+
let command = notification.command;
|
|
1853
|
+
if (command.mailboxCopy && this.notificationQueue.some(queued => queued.mailboxCopy)) {
|
|
1854
|
+
// Only the oldest snapshot describes what this session currently sees,
|
|
1855
|
+
// so do not keep another copy of the message list around
|
|
1856
|
+
command = Object.assign({}, command);
|
|
1857
|
+
delete command.mailboxCopy;
|
|
1858
|
+
}
|
|
1859
|
+
this.queueNotification(command, notification);
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
/**
|
|
1863
|
+
* Queues a notification for this session, it is sent before the next tagged response that allows it, or
|
|
1864
|
+
* right away while notifications are direct (IDLE). Plugins can replace it per connection to drop
|
|
1865
|
+
* notifications or send them at other times (e.g. NOTIFY)
|
|
1866
|
+
*
|
|
1867
|
+
* @param {Object} command Untagged response
|
|
1868
|
+
* @param {Object} notification The `notify` event, `{ command, mailbox, ignoreConnection, filter, origin }`
|
|
1869
|
+
*/
|
|
1870
|
+
queueNotification(command, notification) {
|
|
1871
|
+
this.notificationQueue.push(command);
|
|
1872
|
+
if (this.directNotifications) {
|
|
1873
|
+
this.processNotifications();
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
upgradeConnection(callback) {
|
|
1877
|
+
this.upgrading = true;
|
|
1878
|
+
// Anything the client sent after STARTTLS in plaintext must not be executed after the upgrade (RFC 9051
|
|
1879
|
+
// section 6.2.1). STARTTLS is refused when input is waiting (the noPipelining command option), this only
|
|
1880
|
+
// guards against plugins that upgrade the connection otherwise
|
|
1881
|
+
this.discardInput();
|
|
1882
|
+
const secureContext = tls.createSecureContext(this.server.getCredentials());
|
|
1883
|
+
const socketOptions = {
|
|
1884
|
+
secureContext: secureContext,
|
|
1885
|
+
isServer: true,
|
|
1886
|
+
server: this.server.server,
|
|
1887
|
+
// throws if SNICallback is missing, so we set a default callback
|
|
1888
|
+
SNICallback: function (servername, cb) {
|
|
1889
|
+
cb(null, secureContext);
|
|
1890
|
+
}
|
|
1891
|
+
};
|
|
1892
|
+
// STARTTLS runs on a live connection
|
|
1893
|
+
const socket = this.socket;
|
|
1894
|
+
// remove all listeners from the original socket besides the error handler
|
|
1895
|
+
socket.removeAllListeners();
|
|
1896
|
+
socket.on('error', this.onError.bind(this));
|
|
1897
|
+
// upgrade connection
|
|
1898
|
+
const secureSocket = new tls.TLSSocket(socket, socketOptions);
|
|
1899
|
+
const onTLSError = (err) => {
|
|
1900
|
+
// a failed handshake leaves nothing to talk to, so drop the connection
|
|
1901
|
+
if (this.options.debug) {
|
|
1902
|
+
console.log('TLS error: %s', err.message);
|
|
1903
|
+
}
|
|
1904
|
+
secureSocket.destroy();
|
|
1905
|
+
};
|
|
1906
|
+
secureSocket.on('close', this.onClose.bind(this));
|
|
1907
|
+
secureSocket.on('error', onTLSError);
|
|
1908
|
+
secureSocket.on('clientError', onTLSError);
|
|
1909
|
+
secureSocket.on('secure', () => {
|
|
1910
|
+
this.secureConnection = true;
|
|
1911
|
+
this.socket = secureSocket;
|
|
1912
|
+
this.upgrading = false;
|
|
1913
|
+
secureSocket.on('data', this.receive.bind(this));
|
|
1914
|
+
callback();
|
|
1915
|
+
});
|
|
1916
|
+
}
|
|
1917
|
+
/**
|
|
1918
|
+
* Turns the queued notifications into the responses to send. Plugins can replace it per connection
|
|
1919
|
+
* to report changes in another form (e.g. VANISHED instead of EXPUNGE with QRESYNC)
|
|
1920
|
+
*
|
|
1921
|
+
* @param {Array} queue Queued notifications
|
|
1922
|
+
* @return {Array} Notifications to send
|
|
1923
|
+
*/
|
|
1924
|
+
prepareNotifications(queue) {
|
|
1925
|
+
return queue;
|
|
1926
|
+
}
|
|
1927
|
+
processNotifications(data) {
|
|
1928
|
+
const options = data && this.server.getCommandOptions(data.command);
|
|
1929
|
+
if (options && (options.noExpunge || (options.searchCriteria !== false && this.usesSequenceNumbers(data)))) {
|
|
1930
|
+
// EXPUNGE responses are not allowed during FETCH, STORE and SEARCH (RFC 3501 section 7.4.1), during
|
|
1931
|
+
// the commands that extensions add to this list (see the noExpunge command option), nor during UID
|
|
1932
|
+
// SEARCH with message numbers in the search criteria (RFC 7162 section 3.2.10.2 for VANISHED, EXPUNGE
|
|
1933
|
+
// may wait as well, RFC 3501 only allows it during UID commands)
|
|
1934
|
+
return;
|
|
1935
|
+
}
|
|
1936
|
+
if (!this.notificationQueue.length) {
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1939
|
+
const queue = this.prepareNotifications(this.notificationQueue);
|
|
1940
|
+
this.notificationQueue = [];
|
|
1941
|
+
// Flag updates use the sequence numbers this session knows: before the EXPUNGE responses of
|
|
1942
|
+
// the snapshot are sent, the snapshot, afterwards the current message list
|
|
1943
|
+
const snapshotIndex = queue.findIndex(notification => notification.mailboxCopy);
|
|
1944
|
+
const sequenceMaps = new Map();
|
|
1945
|
+
const getSequence = (messages) => {
|
|
1946
|
+
let sequence = sequenceMaps.get(messages);
|
|
1947
|
+
if (!sequence) {
|
|
1948
|
+
sequence = new Map(messages.map((message, i) => [message, i + 1]));
|
|
1949
|
+
sequenceMaps.set(messages, sequence);
|
|
1950
|
+
}
|
|
1951
|
+
return sequence;
|
|
1952
|
+
};
|
|
1953
|
+
const current = this.selectedMailbox ? this.selectedMailbox.messages : [];
|
|
1954
|
+
// a message changed several times is reported once, its FETCH response carries the current flags
|
|
1955
|
+
const reported = new Set();
|
|
1956
|
+
queue.forEach((notification, i) => {
|
|
1957
|
+
if (notification.flagUpdate) {
|
|
1958
|
+
// i < snapshotIndex only when there is a snapshot
|
|
1959
|
+
this.sendFlagUpdate(notification.flagUpdate, getSequence(i < snapshotIndex ? queue[snapshotIndex].mailboxCopy : current), reported);
|
|
1960
|
+
}
|
|
1961
|
+
else {
|
|
1962
|
+
this.send(notification);
|
|
1963
|
+
}
|
|
1964
|
+
});
|
|
1965
|
+
}
|
|
1966
|
+
/**
|
|
1967
|
+
* Compile a command object to a response string and write it to socket.
|
|
1968
|
+
* If the command object has a skipResponse property, the command is
|
|
1969
|
+
* ignored
|
|
1970
|
+
*
|
|
1971
|
+
* @param {Object} response Response IMAP command object to be compiled.
|
|
1972
|
+
* @param {String} description
|
|
1973
|
+
* An upper-case string uniquely identifying the response for the benefit of
|
|
1974
|
+
* output handlers that wish to augment/replace the given response.
|
|
1975
|
+
* @param {Object} parsed
|
|
1976
|
+
* Original parsed IMAP command that this is in response to.
|
|
1977
|
+
* @param {String} data
|
|
1978
|
+
* Original raw IMAP command as a binary string.
|
|
1979
|
+
* @param {Object} extra
|
|
1980
|
+
* Response-specific payload, usually the subject of the response. For
|
|
1981
|
+
* example, the STORE command will pass the impacted message for each updated
|
|
1982
|
+
* FETCH result. (This may have other names when used, like "affected".)
|
|
1983
|
+
*/
|
|
1984
|
+
send(response, description, parsed, data, ...extra) {
|
|
1985
|
+
// nothing goes out once the connection is closing (RFC 3501 section 7.1.5)
|
|
1986
|
+
if (!this.socket || this.socket.destroyed || this._closing) {
|
|
1987
|
+
return;
|
|
1988
|
+
}
|
|
1989
|
+
if (!response.notification && response.tag !== '*') {
|
|
1990
|
+
// arguments[2] should be the original command
|
|
1991
|
+
this.processNotifications(parsed);
|
|
1992
|
+
}
|
|
1993
|
+
this.server.outputHandlers.forEach(handler => {
|
|
1994
|
+
handler(this, response, description, parsed, data, ...extra);
|
|
1995
|
+
});
|
|
1996
|
+
// No need to display this response to user
|
|
1997
|
+
if (response.skipResponse) {
|
|
1998
|
+
return;
|
|
1999
|
+
}
|
|
2000
|
+
if (this.notificationQueue.length &&
|
|
2001
|
+
parsed &&
|
|
2002
|
+
response.tag === parsed.tag &&
|
|
2003
|
+
response.command === 'OK' &&
|
|
2004
|
+
this.hasPendingExpunge() &&
|
|
2005
|
+
this.server.getCommandOptions(parsed.command).noExpunge &&
|
|
2006
|
+
!(Array.isArray(response.attributes) && response.attributes.some(attr => attr && attr.type === 'SECTION'))) {
|
|
2007
|
+
// After the output handlers, they might add a response code of their own (MODIFIED of CONDSTORE).
|
|
2008
|
+
// FETCH, STORE, SEARCH and the like can not report the EXPUNGE of another session (RFC 3501 section 7.4.1),
|
|
2009
|
+
// EXPUNGEISSUED tells the client to issue NOOP soon (RFC 5530 section 3, RFC 9051 section 7.1)
|
|
2010
|
+
response.attributes = [{ type: 'SECTION', section: [{ type: 'ATOM', value: 'EXPUNGEISSUED' }] }].concat(response.attributes || []);
|
|
2011
|
+
}
|
|
2012
|
+
// a { type: 'MAILBOX', value } attribute holds a storage name, sent in the form this session uses. It
|
|
2013
|
+
// can also be in a list, like the MAILBOX correlator of an ESEARCH response (RFC 7377 section 4)
|
|
2014
|
+
const isMailbox = (attr) => attr && attr.type === 'MAILBOX';
|
|
2015
|
+
const hasMailbox = (list) => list.some((attr) => isMailbox(attr) || (Array.isArray(attr) && hasMailbox(attr)));
|
|
2016
|
+
const exportList = (list) => list.map((attr) => isMailbox(attr) ? mailboxAttribute(this.exportMailboxName(attr.value)) : Array.isArray(attr) ? exportList(attr) : attr);
|
|
2017
|
+
if (Array.isArray(response.attributes) && hasMailbox(response.attributes)) {
|
|
2018
|
+
response = Object.assign({}, response, { attributes: exportList(response.attributes) });
|
|
2019
|
+
}
|
|
2020
|
+
// RFC 3501 section 9: TEXT-CHAR is 7-bit (CHAR = %x01-7F), so client input echoed in the
|
|
2021
|
+
// human readable text of a status response must not carry 8-bit or control octets
|
|
2022
|
+
if (STATUS_RESPONSES.has((response.command || '').toString().toUpperCase()) && Array.isArray(response.attributes)) {
|
|
2023
|
+
const isUnsafe = (attr) => attr && attr.type === 'TEXT' && typeof attr.value === 'string' && /[^\x20-\x7e]/.test(attr.value);
|
|
2024
|
+
if (response.attributes.some(isUnsafe)) {
|
|
2025
|
+
response = Object.assign({}, response, {
|
|
2026
|
+
attributes: response.attributes.map((attr) => isUnsafe(attr) ? Object.assign({}, attr, { value: attr.value.replace(/[^\x20-\x7e]/g, '?') }) : attr)
|
|
2027
|
+
});
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
let compiled;
|
|
2031
|
+
try {
|
|
2032
|
+
compiled = imapHandler.compiler(response, this.compilerOptions);
|
|
2033
|
+
}
|
|
2034
|
+
catch (err) {
|
|
2035
|
+
// the compiler refuses unsafe output, like line breaks in a TEXT value
|
|
2036
|
+
if (this.options.debug) {
|
|
2037
|
+
console.log('Failed to compile response: %s', err.message);
|
|
2038
|
+
}
|
|
2039
|
+
if (response.tag === '*') {
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
2042
|
+
compiled = response.tag + ' NO [SERVERBUG] Failed to compile response';
|
|
2043
|
+
}
|
|
2044
|
+
if (this.options.debug) {
|
|
2045
|
+
console.log('SEND: %s', compiled);
|
|
2046
|
+
}
|
|
2047
|
+
this.write(compiled + '\r\n');
|
|
2048
|
+
}
|
|
2049
|
+
/**
|
|
2050
|
+
* Sends a tagged status response to a command
|
|
2051
|
+
*
|
|
2052
|
+
* @param {Object} parsed Parsed command
|
|
2053
|
+
* @param {String} data Raw command
|
|
2054
|
+
* @param {String} command Response type: OK, NO or BAD
|
|
2055
|
+
* @param {String} text Human readable text
|
|
2056
|
+
* @param {String|Array} [code] Response code, eg. "TRYCREATE", sent as [TRYCREATE], or a list of
|
|
2057
|
+
* atoms like ["METADATA", "MAXSIZE", 1024], sent as [METADATA MAXSIZE 1024]
|
|
2058
|
+
* @param {String} [description] Description for output handlers, defaults to the command name,
|
|
2059
|
+
* with " FAILED" appended for NO and BAD
|
|
2060
|
+
*/
|
|
2061
|
+
sendStatus(parsed, data, command, text, code, description) {
|
|
2062
|
+
const attributes = [];
|
|
2063
|
+
if (code) {
|
|
2064
|
+
attributes.push({
|
|
2065
|
+
type: 'SECTION',
|
|
2066
|
+
section: [].concat(code).map(value => ({
|
|
2067
|
+
type: 'ATOM',
|
|
2068
|
+
value: String(value)
|
|
2069
|
+
}))
|
|
2070
|
+
});
|
|
2071
|
+
}
|
|
2072
|
+
attributes.push({
|
|
2073
|
+
type: 'TEXT',
|
|
2074
|
+
value: text
|
|
2075
|
+
});
|
|
2076
|
+
if (!description) {
|
|
2077
|
+
description = (parsed.command || '').toString().toUpperCase() + (command === 'OK' ? '' : ' FAILED');
|
|
2078
|
+
}
|
|
2079
|
+
this.send({
|
|
2080
|
+
tag: parsed.tag,
|
|
2081
|
+
command,
|
|
2082
|
+
attributes
|
|
2083
|
+
}, description, parsed, data);
|
|
2084
|
+
}
|
|
2085
|
+
/**
|
|
2086
|
+
* Checks if a command was sent without waiting for an earlier command in a way that RFC 3501
|
|
2087
|
+
* section 5.5 forbids: after any command other than FETCH, STORE or SEARCH (or another command with
|
|
2088
|
+
* the noExpunge option, like SORT and THREAD from RFC 5256) the client must
|
|
2089
|
+
* wait for the completion result before it sends a command with message sequence numbers,
|
|
2090
|
+
* because an EXPUNGE response could change what the numbers refer to. A command was sent
|
|
2091
|
+
* without waiting if such a command is still queued or running, or if it completed in the
|
|
2092
|
+
* same read as this command arrived in.
|
|
2093
|
+
*
|
|
2094
|
+
* @param {Object} parsed Parsed command
|
|
2095
|
+
* @return {Boolean} true if the command is ambiguous
|
|
2096
|
+
*/
|
|
2097
|
+
isAmbiguous(parsed) {
|
|
2098
|
+
if (!this.usesSequenceNumbers(parsed)) {
|
|
2099
|
+
return false;
|
|
2100
|
+
}
|
|
2101
|
+
if (this._unsafeCompletedRead === this._readCount) {
|
|
2102
|
+
return true;
|
|
2103
|
+
}
|
|
2104
|
+
const isUnsafe = (element) => !!element && !this.server.getCommandOptions(element.parsed.command).noExpunge;
|
|
2105
|
+
return isUnsafe(this._runningCommand) || this._commandQueue.some(isUnsafe);
|
|
2106
|
+
}
|
|
2107
|
+
/**
|
|
2108
|
+
* Checks if a command refers to messages by sequence number (RFC 3501 section 5.5)
|
|
2109
|
+
*
|
|
2110
|
+
* @param {Object} parsed Parsed command
|
|
2111
|
+
* @return {Boolean} true if the command uses message sequence numbers
|
|
2112
|
+
*/
|
|
2113
|
+
usesSequenceNumbers(parsed) {
|
|
2114
|
+
const { sequenceSet, searchCriteria } = this.server.getCommandOptions(parsed.command);
|
|
2115
|
+
if (sequenceSet !== false) {
|
|
2116
|
+
// other forms of sequence sets, like "$" of SEARCHRES (RFC 5182 section 2.3), do not use numbers
|
|
2117
|
+
const value = parsed.attributes && parsed.attributes[sequenceSet];
|
|
2118
|
+
return !value || /^[\d*]/.test(String(value.value));
|
|
2119
|
+
}
|
|
2120
|
+
if (searchCriteria !== false) {
|
|
2121
|
+
return hasSequenceSetKey(this.server, (parsed.attributes || []).slice(searchCriteria));
|
|
2122
|
+
}
|
|
2123
|
+
return false;
|
|
2124
|
+
}
|
|
2125
|
+
/**
|
|
2126
|
+
* Decodes a SASL client response. It must be valid base64 by the RFC 3501 section 9 grammar,
|
|
2127
|
+
* "=" stands for an empty initial response (RFC 4959 section 3).
|
|
2128
|
+
*
|
|
2129
|
+
* @param {String} str Client response
|
|
2130
|
+
* @return {Buffer|Boolean} Decoded value, or false if the input is not valid base64
|
|
2131
|
+
*/
|
|
2132
|
+
decodeSaslResponse(str) {
|
|
2133
|
+
if (str === '=') {
|
|
2134
|
+
return Buffer.alloc(0);
|
|
2135
|
+
}
|
|
2136
|
+
if (typeof str !== 'string' || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(str)) {
|
|
2137
|
+
return false;
|
|
2138
|
+
}
|
|
2139
|
+
return Buffer.from(str, 'base64');
|
|
2140
|
+
}
|
|
2141
|
+
/**
|
|
2142
|
+
* Returns the target mailbox of APPEND, COPY or MOVE. If messages can not be added
|
|
2143
|
+
* to it, a tagged NO is sent and false is returned
|
|
2144
|
+
*
|
|
2145
|
+
* @param {String} path Mailbox path
|
|
2146
|
+
* @param {Object} parsed Parsed command
|
|
2147
|
+
* @param {String} data Raw command
|
|
2148
|
+
* @param {String} description Description for the failure response
|
|
2149
|
+
* @return {Object|false} Mailbox object
|
|
2150
|
+
*/
|
|
2151
|
+
getTargetMailbox(path, parsed, data, description) {
|
|
2152
|
+
const refusal = this.server.targetRefusal(path);
|
|
2153
|
+
if (refusal) {
|
|
2154
|
+
this.sendStatus(parsed, data, refusal.command, refusal.text, refusal.code, description);
|
|
2155
|
+
return false;
|
|
2156
|
+
}
|
|
2157
|
+
// targetRefusal() found it
|
|
2158
|
+
return this.server.getMailbox(path);
|
|
2159
|
+
}
|
|
2160
|
+
/**
|
|
2161
|
+
* Runs the checks of `server.appendChecks` before messages are added to a mailbox. A check
|
|
2162
|
+
* returns nothing if the messages may be added, or `{ code, text, soft }`: a hard failure is
|
|
2163
|
+
* sent as a tagged NO with the response code and false is returned, a soft one (`soft: true`)
|
|
2164
|
+
* only as an untagged NO warning, e.g. `* NO [OVERQUOTA] ...` (RFC 9208 section 4.3.1).
|
|
2165
|
+
*
|
|
2166
|
+
* @param {Object} mailbox Target mailbox
|
|
2167
|
+
* @param {Array} messages Messages to add, objects with the message source as `raw`
|
|
2168
|
+
* @param {Object} parsed Parsed command
|
|
2169
|
+
* @param {String} data Raw command
|
|
2170
|
+
* @param {String} description Description for the failure response
|
|
2171
|
+
* @param {Object} [options] `{ move, source }`, set for MOVE with the source mailbox, or `{ command, replaced }`
|
|
2172
|
+
* for APPEND like commands (APPEND, REPLACE), `replaced` is the message that REPLACE removes
|
|
2173
|
+
* @return {Boolean} true if the messages may be added
|
|
2174
|
+
*/
|
|
2175
|
+
checkAppend(mailbox, messages, parsed, data, description, options) {
|
|
2176
|
+
return this.applyChecks(this.server.appendChecks.map(check => check(this, mailbox, messages, options || {})), parsed, data, description);
|
|
2177
|
+
}
|
|
2178
|
+
/**
|
|
2179
|
+
* Reports the results of checks like `server.appendChecks`: the first hard failure as a tagged
|
|
2180
|
+
* NO, or soft ones as untagged NO warnings
|
|
2181
|
+
*
|
|
2182
|
+
* @param {Array} results Check results, `{ code, text, soft }` or nothing
|
|
2183
|
+
* @param {Object} parsed Parsed command
|
|
2184
|
+
* @param {String} data Raw command
|
|
2185
|
+
* @param {String} description Description for the failure response
|
|
2186
|
+
* @return {Boolean} false if the command failed
|
|
2187
|
+
*/
|
|
2188
|
+
applyChecks(results, parsed, data, description) {
|
|
2189
|
+
const found = results.filter((result) => !!result);
|
|
2190
|
+
const failure = found.find(result => !result.soft);
|
|
2191
|
+
if (failure) {
|
|
2192
|
+
this.sendStatus(parsed, data, 'NO', failure.text, failure.code, description);
|
|
2193
|
+
return false;
|
|
2194
|
+
}
|
|
2195
|
+
found.forEach(result => {
|
|
2196
|
+
this.send({
|
|
2197
|
+
tag: '*',
|
|
2198
|
+
command: 'NO',
|
|
2199
|
+
attributes: [
|
|
2200
|
+
{ type: 'SECTION', section: [{ type: 'ATOM', value: result.code }] },
|
|
2201
|
+
{ type: 'TEXT', value: result.text }
|
|
2202
|
+
]
|
|
2203
|
+
}, 'CHECK WARNING', parsed, data);
|
|
2204
|
+
});
|
|
2205
|
+
return true;
|
|
2206
|
+
}
|
|
2207
|
+
/**
|
|
2208
|
+
* Converts a mailbox name from a command to the name used in storage, which is modified UTF-7
|
|
2209
|
+
* (RFC 3501 section 5.1.3). A plugin can replace this per connection, e.g. UTF8=ACCEPT.
|
|
2210
|
+
*
|
|
2211
|
+
* @param {String} name Mailbox name as a binary string
|
|
2212
|
+
* @return {String} Storage name
|
|
2213
|
+
* @throws {Error} BAD error if the name is not valid
|
|
2214
|
+
*/
|
|
2215
|
+
importMailboxName(name) {
|
|
2216
|
+
const error = validateMailboxName(name);
|
|
2217
|
+
if (error) {
|
|
2218
|
+
const err = new Error(error);
|
|
2219
|
+
err.imapResponse = 'BAD';
|
|
2220
|
+
throw err;
|
|
2221
|
+
}
|
|
2222
|
+
return name;
|
|
2223
|
+
}
|
|
2224
|
+
/**
|
|
2225
|
+
* Converts a mailbox name from storage to the form sent to the client. Every response that
|
|
2226
|
+
* includes a mailbox name must use this. A plugin can replace this per connection.
|
|
2227
|
+
*
|
|
2228
|
+
* @param {String} path Storage name
|
|
2229
|
+
* @return {String} Mailbox name as a binary string
|
|
2230
|
+
*/
|
|
2231
|
+
exportMailboxName(path) {
|
|
2232
|
+
return path;
|
|
2233
|
+
}
|
|
2234
|
+
scheduleCommand(data) {
|
|
2235
|
+
let parsed;
|
|
2236
|
+
const tag = getResponseTag(data);
|
|
2237
|
+
try {
|
|
2238
|
+
// server.parserOptions are the defaults of plugins, connection.parserOptions win
|
|
2239
|
+
parsed = imapHandler.parser(data, Object.assign({ literalPlus: this.server.literalPlus }, this.server.parserOptions, this.parserOptions));
|
|
2240
|
+
}
|
|
2241
|
+
catch (E) {
|
|
2242
|
+
const error = E;
|
|
2243
|
+
this.send({
|
|
2244
|
+
tag: '*',
|
|
2245
|
+
command: 'BAD',
|
|
2246
|
+
attributes: [
|
|
2247
|
+
{
|
|
2248
|
+
type: 'SECTION',
|
|
2249
|
+
section: [
|
|
2250
|
+
{
|
|
2251
|
+
type: 'ATOM',
|
|
2252
|
+
value: 'SYNTAX'
|
|
2253
|
+
}
|
|
2254
|
+
]
|
|
2255
|
+
},
|
|
2256
|
+
{
|
|
2257
|
+
type: 'TEXT',
|
|
2258
|
+
value: error.message
|
|
2259
|
+
}
|
|
2260
|
+
]
|
|
2261
|
+
}, 'ERROR MESSAGE', null, data, error);
|
|
2262
|
+
this.send({
|
|
2263
|
+
tag: tag,
|
|
2264
|
+
command: 'BAD',
|
|
2265
|
+
attributes: [
|
|
2266
|
+
{
|
|
2267
|
+
type: 'TEXT',
|
|
2268
|
+
value: 'Error parsing command'
|
|
2269
|
+
}
|
|
2270
|
+
]
|
|
2271
|
+
}, 'ERROR RESPONSE', null, data, error);
|
|
2272
|
+
return;
|
|
2273
|
+
}
|
|
2274
|
+
if (this.isPipelinedAfterRefusal()) {
|
|
2275
|
+
this.refusePipelined(parsed, data);
|
|
2276
|
+
return;
|
|
2277
|
+
}
|
|
2278
|
+
if (this.server.getCommandHandler(parsed.command)) {
|
|
2279
|
+
if (this.isAmbiguous(parsed)) {
|
|
2280
|
+
this.sendStatus(parsed, data, 'BAD', 'Commands with message sequence numbers must wait for the completion of earlier commands');
|
|
2281
|
+
return;
|
|
2282
|
+
}
|
|
2283
|
+
this._commandQueue.push({
|
|
2284
|
+
parsed: parsed,
|
|
2285
|
+
data: data
|
|
2286
|
+
});
|
|
2287
|
+
this.processQueue();
|
|
2288
|
+
}
|
|
2289
|
+
else if (/^AUTHENTICATE /i.test(parsed.command)) {
|
|
2290
|
+
// an unsupported mechanism is a NO, not a syntax error (RFC 3501 section 6.2.2)
|
|
2291
|
+
this.send({
|
|
2292
|
+
tag: parsed.tag,
|
|
2293
|
+
command: 'NO',
|
|
2294
|
+
attributes: [
|
|
2295
|
+
{
|
|
2296
|
+
type: 'TEXT',
|
|
2297
|
+
value: 'Unsupported authentication mechanism'
|
|
2298
|
+
}
|
|
2299
|
+
]
|
|
2300
|
+
}, 'UNKNOWN COMMAND', parsed, data);
|
|
2301
|
+
}
|
|
2302
|
+
else {
|
|
2303
|
+
this.send({
|
|
2304
|
+
tag: parsed.tag,
|
|
2305
|
+
command: 'BAD',
|
|
2306
|
+
attributes: [
|
|
2307
|
+
{
|
|
2308
|
+
type: 'TEXT',
|
|
2309
|
+
value: 'Invalid command ' + parsed.command + ''
|
|
2310
|
+
}
|
|
2311
|
+
]
|
|
2312
|
+
}, 'UNKNOWN COMMAND', parsed, data);
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
processQueue(force) {
|
|
2316
|
+
if (!force && this._processing) {
|
|
2317
|
+
return;
|
|
2318
|
+
}
|
|
2319
|
+
if (!this._commandQueue.length) {
|
|
2320
|
+
this._processing = false;
|
|
2321
|
+
return;
|
|
2322
|
+
}
|
|
2323
|
+
this._processing = true;
|
|
2324
|
+
const element = this._commandQueue.shift();
|
|
2325
|
+
const command = element.parsed.command.toUpperCase();
|
|
2326
|
+
this._runningCommand = element;
|
|
2327
|
+
const options = this.server.getCommandOptions(command);
|
|
2328
|
+
let done = false;
|
|
2329
|
+
const next = () => {
|
|
2330
|
+
if (done) {
|
|
2331
|
+
// a handler must release the queue only once
|
|
2332
|
+
return;
|
|
2333
|
+
}
|
|
2334
|
+
done = true;
|
|
2335
|
+
this._runningCommand = null;
|
|
2336
|
+
if (!options.noExpunge) {
|
|
2337
|
+
// commands with sequence numbers that arrive in the same read did not wait for this one
|
|
2338
|
+
this._unsafeCompletedRead = this._readCount;
|
|
2339
|
+
}
|
|
2340
|
+
if (!this._commandQueue.length) {
|
|
2341
|
+
this._processing = false;
|
|
2342
|
+
}
|
|
2343
|
+
else {
|
|
2344
|
+
this.processQueue(true);
|
|
2345
|
+
}
|
|
2346
|
+
};
|
|
2347
|
+
if (options.states && options.states.indexOf(this.state) < 0) {
|
|
2348
|
+
this.sendStatus(element.parsed, element.data, 'BAD', stateError(command, this.state));
|
|
2349
|
+
return next();
|
|
2350
|
+
}
|
|
2351
|
+
if (element.parsed.attributes && options.noArguments) {
|
|
2352
|
+
this.sendStatus(element.parsed, element.data, 'BAD', command + ' does not take any arguments');
|
|
2353
|
+
return next();
|
|
2354
|
+
}
|
|
2355
|
+
if (options.noPipelining && this.hasPendingInput()) {
|
|
2356
|
+
// the layer change is not made. The commands that follow were meant to run in the new layer (under TLS, or
|
|
2357
|
+
// compressed), so none of them runs: they are refused like this command, see refusePipelined
|
|
2358
|
+
this.sendStatus(element.parsed, element.data, 'BAD', 'Commands must not be pipelined after ' + command);
|
|
2359
|
+
this._pipelinedAfter = { command, read: this._readCount };
|
|
2360
|
+
this._commandQueue.splice(0).forEach(queued => this.refusePipelined(queued.parsed, queued.data));
|
|
2361
|
+
return next();
|
|
2362
|
+
}
|
|
2363
|
+
// the parser reads every NIL atom as nil, but in an astring NIL is just a name (e.g. SELECT NIL)
|
|
2364
|
+
restoreNilAtoms(element.parsed, element.data, path => (path.length === 1 && (options.mailboxArguments.includes(path[0]) || options.astringArguments.includes(path[0]))) ||
|
|
2365
|
+
(options.searchCriteria !== false && path[0] >= options.searchCriteria));
|
|
2366
|
+
const nameError = importMailboxArguments(this, element.parsed, options.mailboxArguments);
|
|
2367
|
+
if (nameError) {
|
|
2368
|
+
this.sendStatus(element.parsed, element.data, 'BAD', nameError);
|
|
2369
|
+
return next();
|
|
2370
|
+
}
|
|
2371
|
+
for (const check of this.server.commandChecks) {
|
|
2372
|
+
const refusal = check(this, element.parsed);
|
|
2373
|
+
if (refusal) {
|
|
2374
|
+
this.sendStatus(element.parsed, element.data, refusal.command || 'BAD', refusal.text, refusal.code);
|
|
2375
|
+
return next();
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
if (command.substr(0, 4) === 'UID ' && this.hasPendingExpunge()) {
|
|
2379
|
+
// EXPUNGE responses may be sent during UID commands (RFC 3501 section 7.4.1). The expunges of other sessions
|
|
2380
|
+
// are reported first, then the command runs on the current mailbox, where the UIDs of the expunged messages
|
|
2381
|
+
// do not exist and are ignored (RFC 3501 section 6.4.8), so the ghost handling of STORE, COPY and MOVE (RFC 2180
|
|
2382
|
+
// section 4) only applies to their sequence number forms. Not for UID SEARCH with message numbers in the
|
|
2383
|
+
// criteria, processNotifications knows when EXPUNGE must wait
|
|
2384
|
+
this.processNotifications(element.parsed);
|
|
2385
|
+
}
|
|
2386
|
+
try {
|
|
2387
|
+
// changes made while the handler runs are attributed to this session (the `origin` of notifications)
|
|
2388
|
+
this.server.activeConnection = this;
|
|
2389
|
+
this.server.getCommandHandler(element.parsed.command)(this, element.parsed, element.data, next);
|
|
2390
|
+
}
|
|
2391
|
+
catch (E) {
|
|
2392
|
+
const ex = E;
|
|
2393
|
+
const badInput = ex.imapResponse === 'BAD';
|
|
2394
|
+
if (!badInput && this.options.debug) {
|
|
2395
|
+
console.error('Error processing command:', ex, '\n', ex.stack);
|
|
2396
|
+
}
|
|
2397
|
+
this.send({
|
|
2398
|
+
tag: element.parsed.tag,
|
|
2399
|
+
command: badInput ? 'BAD' : 'NO',
|
|
2400
|
+
attributes: [].concat(badInput
|
|
2401
|
+
? []
|
|
2402
|
+
: {
|
|
2403
|
+
type: 'SECTION',
|
|
2404
|
+
section: [
|
|
2405
|
+
{
|
|
2406
|
+
type: 'ATOM',
|
|
2407
|
+
value: 'SERVERBUG'
|
|
2408
|
+
}
|
|
2409
|
+
]
|
|
2410
|
+
}, {
|
|
2411
|
+
type: 'TEXT',
|
|
2412
|
+
value: badInput ? ex.message : 'Server error: ' + ex.message
|
|
2413
|
+
})
|
|
2414
|
+
}, badInput ? 'INVALID COMMAND' : 'SERVER ERROR', element.parsed, element.data);
|
|
2415
|
+
// keep the connection usable, otherwise every later command would hang
|
|
2416
|
+
next();
|
|
2417
|
+
}
|
|
2418
|
+
finally {
|
|
2419
|
+
this.server.activeConnection = null;
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
/**
|
|
2423
|
+
* Removes messages with \Deleted flag
|
|
2424
|
+
*
|
|
2425
|
+
* @param {Object} mailbox Mailbox to check for
|
|
2426
|
+
* @param {Boolean} [ignoreSelf] If set to true, does not send any notices to itself
|
|
2427
|
+
* @param {Boolean} [ignoreSelf] If set to true, does not send EXISTS notice to itself
|
|
2428
|
+
*/
|
|
2429
|
+
expungeDeleted(mailbox, ignoreSelf, ignoreExists) {
|
|
2430
|
+
this.expungeSpecificMessages(mailbox, (message) => {
|
|
2431
|
+
return message.flags.indexOf('\\Deleted') >= 0;
|
|
2432
|
+
}, ignoreSelf, ignoreExists);
|
|
2433
|
+
}
|
|
2434
|
+
/**
|
|
2435
|
+
* Given a set of messages in a mailbox (possibly via getMessageRange), remove
|
|
2436
|
+
* them from the mailbox and generate EXPUNGE notifications.
|
|
2437
|
+
*
|
|
2438
|
+
* @param {Object} mailbox Mailbox to check for
|
|
2439
|
+
* @param {Function|Array} messagesOrFilterFunc An Array of messages in the
|
|
2440
|
+
* folder that should be removed or a filtering function that indicates
|
|
2441
|
+
* messages to be removed by returning true.
|
|
2442
|
+
* @param {Boolean} [ignoreSelf] If set to true, does not send any notices to itself
|
|
2443
|
+
* @param {Boolean} [ignoreSelf] If set to true, does not send EXISTS notice to itself
|
|
2444
|
+
* @param {Boolean} [highestFirst] If set to true, the EXPUNGE responses go from the highest UID to the lowest
|
|
2445
|
+
* (MESSAGELIMIT, RFC 9738 section 3.1), otherwise from the lowest
|
|
2446
|
+
*/
|
|
2447
|
+
expungeSpecificMessages(mailbox, messagesOrFilterFunc, ignoreSelf, ignoreExists, highestFirst) {
|
|
2448
|
+
let filterFunc;
|
|
2449
|
+
if (Array.isArray(messagesOrFilterFunc)) {
|
|
2450
|
+
const messageSet = new Set(messagesOrFilterFunc);
|
|
2451
|
+
filterFunc = (message) => messageSet.has(message);
|
|
2452
|
+
}
|
|
2453
|
+
else {
|
|
2454
|
+
filterFunc = messagesOrFilterFunc;
|
|
2455
|
+
}
|
|
2456
|
+
// sequence numbers of the removed messages, each one as it is after the earlier EXPUNGE responses. From the
|
|
2457
|
+
// highest message down, the earlier responses do not change the sequence numbers of the later ones
|
|
2458
|
+
const expunged = [];
|
|
2459
|
+
const kept = [];
|
|
2460
|
+
mailbox.messages.forEach((message, i) => {
|
|
2461
|
+
if (filterFunc(message)) {
|
|
2462
|
+
message.ghost = true;
|
|
2463
|
+
expunged.push({ seq: highestFirst ? i + 1 : kept.length + 1, message });
|
|
2464
|
+
}
|
|
2465
|
+
else {
|
|
2466
|
+
kept.push(message);
|
|
2467
|
+
}
|
|
2468
|
+
});
|
|
2469
|
+
if (!expunged.length) {
|
|
2470
|
+
return;
|
|
2471
|
+
}
|
|
2472
|
+
// old copy is required for those sessions that run FETCH before
|
|
2473
|
+
// displaying the EXPUNGE notice
|
|
2474
|
+
const mailboxCopy = mailbox.messages.slice();
|
|
2475
|
+
// update the list in place, other code might hold a reference to it
|
|
2476
|
+
kept.forEach((message, i) => {
|
|
2477
|
+
mailbox.messages[i] = message;
|
|
2478
|
+
});
|
|
2479
|
+
mailbox.messages.length = kept.length;
|
|
2480
|
+
// lets plugins track the removal (e.g. mod-sequences of CONDSTORE and QRESYNC) before any notification
|
|
2481
|
+
this.server.emit('expunge', mailbox, expunged.map(entry => entry.message), this);
|
|
2482
|
+
(highestFirst ? expunged.slice().reverse() : expunged).forEach(entry => {
|
|
2483
|
+
this.server.notify({
|
|
2484
|
+
tag: '*',
|
|
2485
|
+
attributes: [
|
|
2486
|
+
entry.seq,
|
|
2487
|
+
{
|
|
2488
|
+
type: 'ATOM',
|
|
2489
|
+
value: 'EXPUNGE'
|
|
2490
|
+
}
|
|
2491
|
+
],
|
|
2492
|
+
// the removed message, for plugins that report it differently (e.g. VANISHED of QRESYNC)
|
|
2493
|
+
message: entry.message
|
|
2494
|
+
}, mailbox, ignoreSelf ? this : false);
|
|
2495
|
+
});
|
|
2496
|
+
this.server.notify({
|
|
2497
|
+
tag: '*',
|
|
2498
|
+
attributes: [
|
|
2499
|
+
mailbox.messages.length,
|
|
2500
|
+
{
|
|
2501
|
+
type: 'ATOM',
|
|
2502
|
+
value: 'EXISTS'
|
|
2503
|
+
}
|
|
2504
|
+
],
|
|
2505
|
+
// distribute the old mailbox data with the notification
|
|
2506
|
+
mailboxCopy: mailboxCopy
|
|
2507
|
+
}, mailbox, ignoreSelf || ignoreExists ? this : false);
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
/**
|
|
2511
|
+
* Formats a mailbox name for a response: an atom when possible, otherwise a string. NIL and names
|
|
2512
|
+
* like \\Foo would not read back as mailbox names, so these are strings as well
|
|
2513
|
+
*
|
|
2514
|
+
* @param {String} name Mailbox name as sent to the client
|
|
2515
|
+
* @return {Object} Response attribute
|
|
2516
|
+
*/
|
|
2517
|
+
function mailboxAttribute(name) {
|
|
2518
|
+
return { type: ATOM_REGEX.test(name) && !/^NIL$/i.test(name) ? 'ATOM' : 'STRING', value: name };
|
|
2519
|
+
}
|
|
2520
|
+
/**
|
|
2521
|
+
* Checks the mailbox name arguments of a command and replaces them with the storage names.
|
|
2522
|
+
* Arguments that are not strings are left to the command handler.
|
|
2523
|
+
*
|
|
2524
|
+
* @param {Object} connection IMAP connection
|
|
2525
|
+
* @param {Object} parsed Parsed command
|
|
2526
|
+
* @param {Array} positions Argument positions that hold mailbox names
|
|
2527
|
+
* @return {String|Boolean} Description of the problem, or false if the names are valid
|
|
2528
|
+
*/
|
|
2529
|
+
function importMailboxArguments(connection, parsed, positions) {
|
|
2530
|
+
for (const position of positions) {
|
|
2531
|
+
const attr = (parsed.attributes || [])[position];
|
|
2532
|
+
if (attr && ['STRING', 'ATOM', 'LITERAL'].indexOf(attr.type) >= 0) {
|
|
2533
|
+
try {
|
|
2534
|
+
attr.value = connection.importMailboxName(attr.value);
|
|
2535
|
+
}
|
|
2536
|
+
catch (err) {
|
|
2537
|
+
return err.message;
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
return false;
|
|
2542
|
+
}
|
|
2543
|
+
export { TAG_REGEX, IMAPServer, IMAPConnection };
|