@withbuddi/buddi 0.1.0-pre.49 → 0.1.0-pre.51

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.
Files changed (221) hide show
  1. package/extension/manifest.json +2 -2
  2. package/node_modules/@buddi/cli/dist/.tsbuildinfo +1 -1
  3. package/node_modules/@buddi/cli/dist/commands.d.ts.map +1 -1
  4. package/node_modules/@buddi/cli/dist/commands.js +11 -1
  5. package/node_modules/@buddi/cli/dist/commands.js.map +1 -1
  6. package/node_modules/@buddi/cli/dist/main.d.ts.map +1 -1
  7. package/node_modules/@buddi/cli/dist/main.js +3 -1
  8. package/node_modules/@buddi/cli/dist/main.js.map +1 -1
  9. package/node_modules/@buddi/core/dist/.tsbuildinfo +1 -1
  10. package/node_modules/@buddi/core/dist/registry.d.ts.map +1 -1
  11. package/node_modules/@buddi/core/dist/registry.js +11 -2
  12. package/node_modules/@buddi/core/dist/registry.js.map +1 -1
  13. package/node_modules/@buddi/core/dist/scheduler/index.d.ts +1 -0
  14. package/node_modules/@buddi/core/dist/scheduler/index.d.ts.map +1 -1
  15. package/node_modules/@buddi/core/dist/scheduler/index.js +1 -0
  16. package/node_modules/@buddi/core/dist/scheduler/index.js.map +1 -1
  17. package/node_modules/@buddi/core/dist/scheduler/manual.d.ts +50 -0
  18. package/node_modules/@buddi/core/dist/scheduler/manual.d.ts.map +1 -0
  19. package/node_modules/@buddi/core/dist/scheduler/manual.js +73 -0
  20. package/node_modules/@buddi/core/dist/scheduler/manual.js.map +1 -0
  21. package/node_modules/@buddi/gateway/dist/.tsbuildinfo +1 -1
  22. package/node_modules/@buddi/gateway/dist/agents/platform-catalogue.d.ts +2 -2
  23. package/node_modules/@buddi/gateway/dist/mcp/requests.d.ts.map +1 -1
  24. package/node_modules/@buddi/gateway/dist/mcp/requests.js +9 -1
  25. package/node_modules/@buddi/gateway/dist/mcp/requests.js.map +1 -1
  26. package/node_modules/@buddi/gateway/dist/missions/reminders.d.ts +7 -2
  27. package/node_modules/@buddi/gateway/dist/missions/reminders.d.ts.map +1 -1
  28. package/node_modules/@buddi/gateway/dist/missions/reminders.js +27 -9
  29. package/node_modules/@buddi/gateway/dist/missions/reminders.js.map +1 -1
  30. package/node_modules/@buddi/gateway/dist/missions/run-now.d.ts +38 -0
  31. package/node_modules/@buddi/gateway/dist/missions/run-now.d.ts.map +1 -0
  32. package/node_modules/@buddi/gateway/dist/missions/run-now.js +62 -0
  33. package/node_modules/@buddi/gateway/dist/missions/run-now.js.map +1 -0
  34. package/node_modules/@buddi/gateway/dist/missions-cli.d.ts +1 -1
  35. package/node_modules/@buddi/gateway/dist/missions-cli.d.ts.map +1 -1
  36. package/node_modules/@buddi/gateway/dist/missions-cli.js +21 -10
  37. package/node_modules/@buddi/gateway/dist/missions-cli.js.map +1 -1
  38. package/node_modules/@buddi/gateway/dist/plugins/approve.d.ts.map +1 -1
  39. package/node_modules/@buddi/gateway/dist/plugins/approve.js +10 -1
  40. package/node_modules/@buddi/gateway/dist/plugins/approve.js.map +1 -1
  41. package/node_modules/@buddi/gateway/dist/plugins/hash.d.ts +4 -3
  42. package/node_modules/@buddi/gateway/dist/plugins/hash.d.ts.map +1 -1
  43. package/node_modules/@buddi/gateway/dist/plugins/hash.js.map +1 -1
  44. package/node_modules/@buddi/gateway/dist/plugins/index.d.ts +1 -0
  45. package/node_modules/@buddi/gateway/dist/plugins/index.d.ts.map +1 -1
  46. package/node_modules/@buddi/gateway/dist/plugins/index.js +1 -0
  47. package/node_modules/@buddi/gateway/dist/plugins/index.js.map +1 -1
  48. package/node_modules/@buddi/gateway/dist/plugins/load.d.ts +14 -0
  49. package/node_modules/@buddi/gateway/dist/plugins/load.d.ts.map +1 -1
  50. package/node_modules/@buddi/gateway/dist/plugins/load.js +27 -0
  51. package/node_modules/@buddi/gateway/dist/plugins/load.js.map +1 -1
  52. package/node_modules/@buddi/gateway/dist/plugins/relink.d.ts +36 -0
  53. package/node_modules/@buddi/gateway/dist/plugins/relink.d.ts.map +1 -0
  54. package/node_modules/@buddi/gateway/dist/plugins/relink.js +105 -0
  55. package/node_modules/@buddi/gateway/dist/plugins/relink.js.map +1 -0
  56. package/node_modules/@buddi/gateway/dist/plugins/stage.d.ts +18 -0
  57. package/node_modules/@buddi/gateway/dist/plugins/stage.d.ts.map +1 -1
  58. package/node_modules/@buddi/gateway/dist/plugins/stage.js +73 -22
  59. package/node_modules/@buddi/gateway/dist/plugins/stage.js.map +1 -1
  60. package/node_modules/@buddi/gateway/dist/plugins/tree.d.ts +3 -1
  61. package/node_modules/@buddi/gateway/dist/plugins/tree.d.ts.map +1 -1
  62. package/node_modules/@buddi/gateway/dist/plugins/tree.js +4 -2
  63. package/node_modules/@buddi/gateway/dist/plugins/tree.js.map +1 -1
  64. package/node_modules/@buddi/gateway/dist/serve.d.ts +3 -12
  65. package/node_modules/@buddi/gateway/dist/serve.d.ts.map +1 -1
  66. package/node_modules/@buddi/gateway/dist/serve.js +6 -21
  67. package/node_modules/@buddi/gateway/dist/serve.js.map +1 -1
  68. package/node_modules/@buddi/gateway/dist/web/api-routes.d.ts.map +1 -1
  69. package/node_modules/@buddi/gateway/dist/web/api-routes.js +7 -0
  70. package/node_modules/@buddi/gateway/dist/web/api-routes.js.map +1 -1
  71. package/node_modules/@buddi/gateway/dist/web/plugins.d.ts.map +1 -1
  72. package/node_modules/@buddi/gateway/dist/web/plugins.js +3 -0
  73. package/node_modules/@buddi/gateway/dist/web/plugins.js.map +1 -1
  74. package/node_modules/@buddi/gateway/dist/web/read.d.ts +2 -0
  75. package/node_modules/@buddi/gateway/dist/web/read.d.ts.map +1 -1
  76. package/node_modules/@buddi/gateway/dist/web/read.js +2 -1
  77. package/node_modules/@buddi/gateway/dist/web/read.js.map +1 -1
  78. package/node_modules/@buddi/gateway/dist/web/server.d.ts.map +1 -1
  79. package/node_modules/@buddi/gateway/dist/web/server.js +4 -1
  80. package/node_modules/@buddi/gateway/dist/web/server.js.map +1 -1
  81. package/node_modules/@buddi/gateway/dist/web/write.d.ts +14 -0
  82. package/node_modules/@buddi/gateway/dist/web/write.d.ts.map +1 -1
  83. package/node_modules/@buddi/gateway/dist/web/write.js +21 -0
  84. package/node_modules/@buddi/gateway/dist/web/write.js.map +1 -1
  85. package/node_modules/@buddi/install/dist/.tsbuildinfo +1 -1
  86. package/node_modules/@buddi/install/dist/supervisor.d.ts.map +1 -1
  87. package/node_modules/@buddi/install/dist/supervisor.js +1 -0
  88. package/node_modules/@buddi/install/dist/supervisor.js.map +1 -1
  89. package/node_modules/@buddi/install/dist/upgrade.d.ts +9 -0
  90. package/node_modules/@buddi/install/dist/upgrade.d.ts.map +1 -1
  91. package/node_modules/@buddi/install/dist/upgrade.js +20 -0
  92. package/node_modules/@buddi/install/dist/upgrade.js.map +1 -1
  93. package/node_modules/@buddi/runtime/dist/.tsbuildinfo +1 -1
  94. package/node_modules/@buddi/runtime/dist/codex-direct.d.ts +1 -1
  95. package/node_modules/@buddi/runtime/dist/codex-direct.js +1 -1
  96. package/node_modules/imapflow/CHANGELOG.md +15 -0
  97. package/node_modules/imapflow/dist/cjs/commands/authenticate.js +15 -4
  98. package/node_modules/imapflow/dist/cjs/commands/enable.js +6 -0
  99. package/node_modules/imapflow/dist/cjs/commands/fetch.js +22 -5
  100. package/node_modules/imapflow/dist/cjs/commands/store.js +7 -6
  101. package/node_modules/imapflow/dist/cjs/download.js +139 -5
  102. package/node_modules/imapflow/dist/cjs/imap-flow.d.ts +2 -2
  103. package/node_modules/imapflow/dist/cjs/imap-flow.js +21 -6
  104. package/node_modules/imapflow/dist/cjs/package-info.d.ts +1 -1
  105. package/node_modules/imapflow/dist/cjs/package-info.js +1 -1
  106. package/node_modules/imapflow/dist/cjs/search-compiler.js +6 -3
  107. package/node_modules/imapflow/dist/cjs/tools.d.ts +4 -3
  108. package/node_modules/imapflow/dist/cjs/tools.js +39 -6
  109. package/node_modules/imapflow/dist/esm/commands/authenticate.js +15 -4
  110. package/node_modules/imapflow/dist/esm/commands/enable.js +6 -0
  111. package/node_modules/imapflow/dist/esm/commands/fetch.js +22 -5
  112. package/node_modules/imapflow/dist/esm/commands/store.js +8 -7
  113. package/node_modules/imapflow/dist/esm/download.js +139 -5
  114. package/node_modules/imapflow/dist/esm/imap-flow.d.ts +2 -2
  115. package/node_modules/imapflow/dist/esm/imap-flow.js +21 -6
  116. package/node_modules/imapflow/dist/esm/package-info.d.ts +1 -1
  117. package/node_modules/imapflow/dist/esm/package-info.js +1 -1
  118. package/node_modules/imapflow/dist/esm/search-compiler.js +6 -3
  119. package/node_modules/imapflow/dist/esm/tools.d.ts +4 -3
  120. package/node_modules/imapflow/dist/esm/tools.js +39 -6
  121. package/node_modules/imapflow/package.json +5 -3
  122. package/package.json +2 -2
  123. package/packages/cli/dist/.tsbuildinfo +1 -1
  124. package/packages/cli/dist/commands.d.ts.map +1 -1
  125. package/packages/cli/dist/commands.js +11 -1
  126. package/packages/cli/dist/commands.js.map +1 -1
  127. package/packages/cli/dist/main.d.ts.map +1 -1
  128. package/packages/cli/dist/main.js +3 -1
  129. package/packages/cli/dist/main.js.map +1 -1
  130. package/packages/core/dist/.tsbuildinfo +1 -1
  131. package/packages/core/dist/registry.d.ts.map +1 -1
  132. package/packages/core/dist/registry.js +11 -2
  133. package/packages/core/dist/registry.js.map +1 -1
  134. package/packages/core/dist/scheduler/index.d.ts +1 -0
  135. package/packages/core/dist/scheduler/index.d.ts.map +1 -1
  136. package/packages/core/dist/scheduler/index.js +1 -0
  137. package/packages/core/dist/scheduler/index.js.map +1 -1
  138. package/packages/core/dist/scheduler/manual.d.ts +50 -0
  139. package/packages/core/dist/scheduler/manual.d.ts.map +1 -0
  140. package/packages/core/dist/scheduler/manual.js +73 -0
  141. package/packages/core/dist/scheduler/manual.js.map +1 -0
  142. package/packages/gateway/dist/.tsbuildinfo +1 -1
  143. package/packages/gateway/dist/agents/platform-catalogue.d.ts +2 -2
  144. package/packages/gateway/dist/mcp/requests.d.ts.map +1 -1
  145. package/packages/gateway/dist/mcp/requests.js +9 -1
  146. package/packages/gateway/dist/mcp/requests.js.map +1 -1
  147. package/packages/gateway/dist/missions/reminders.d.ts +7 -2
  148. package/packages/gateway/dist/missions/reminders.d.ts.map +1 -1
  149. package/packages/gateway/dist/missions/reminders.js +27 -9
  150. package/packages/gateway/dist/missions/reminders.js.map +1 -1
  151. package/packages/gateway/dist/missions/run-now.d.ts +38 -0
  152. package/packages/gateway/dist/missions/run-now.d.ts.map +1 -0
  153. package/packages/gateway/dist/missions/run-now.js +62 -0
  154. package/packages/gateway/dist/missions/run-now.js.map +1 -0
  155. package/packages/gateway/dist/missions-cli.d.ts +1 -1
  156. package/packages/gateway/dist/missions-cli.d.ts.map +1 -1
  157. package/packages/gateway/dist/missions-cli.js +21 -10
  158. package/packages/gateway/dist/missions-cli.js.map +1 -1
  159. package/packages/gateway/dist/plugins/approve.d.ts.map +1 -1
  160. package/packages/gateway/dist/plugins/approve.js +10 -1
  161. package/packages/gateway/dist/plugins/approve.js.map +1 -1
  162. package/packages/gateway/dist/plugins/hash.d.ts +4 -3
  163. package/packages/gateway/dist/plugins/hash.d.ts.map +1 -1
  164. package/packages/gateway/dist/plugins/hash.js.map +1 -1
  165. package/packages/gateway/dist/plugins/index.d.ts +1 -0
  166. package/packages/gateway/dist/plugins/index.d.ts.map +1 -1
  167. package/packages/gateway/dist/plugins/index.js +1 -0
  168. package/packages/gateway/dist/plugins/index.js.map +1 -1
  169. package/packages/gateway/dist/plugins/load.d.ts +14 -0
  170. package/packages/gateway/dist/plugins/load.d.ts.map +1 -1
  171. package/packages/gateway/dist/plugins/load.js +27 -0
  172. package/packages/gateway/dist/plugins/load.js.map +1 -1
  173. package/packages/gateway/dist/plugins/relink.d.ts +36 -0
  174. package/packages/gateway/dist/plugins/relink.d.ts.map +1 -0
  175. package/packages/gateway/dist/plugins/relink.js +105 -0
  176. package/packages/gateway/dist/plugins/relink.js.map +1 -0
  177. package/packages/gateway/dist/plugins/stage.d.ts +18 -0
  178. package/packages/gateway/dist/plugins/stage.d.ts.map +1 -1
  179. package/packages/gateway/dist/plugins/stage.js +73 -22
  180. package/packages/gateway/dist/plugins/stage.js.map +1 -1
  181. package/packages/gateway/dist/plugins/tree.d.ts +3 -1
  182. package/packages/gateway/dist/plugins/tree.d.ts.map +1 -1
  183. package/packages/gateway/dist/plugins/tree.js +4 -2
  184. package/packages/gateway/dist/plugins/tree.js.map +1 -1
  185. package/packages/gateway/dist/serve.d.ts +3 -12
  186. package/packages/gateway/dist/serve.d.ts.map +1 -1
  187. package/packages/gateway/dist/serve.js +6 -21
  188. package/packages/gateway/dist/serve.js.map +1 -1
  189. package/packages/gateway/dist/web/api-routes.d.ts.map +1 -1
  190. package/packages/gateway/dist/web/api-routes.js +7 -0
  191. package/packages/gateway/dist/web/api-routes.js.map +1 -1
  192. package/packages/gateway/dist/web/plugins.d.ts.map +1 -1
  193. package/packages/gateway/dist/web/plugins.js +3 -0
  194. package/packages/gateway/dist/web/plugins.js.map +1 -1
  195. package/packages/gateway/dist/web/read.d.ts +2 -0
  196. package/packages/gateway/dist/web/read.d.ts.map +1 -1
  197. package/packages/gateway/dist/web/read.js +2 -1
  198. package/packages/gateway/dist/web/read.js.map +1 -1
  199. package/packages/gateway/dist/web/server.d.ts.map +1 -1
  200. package/packages/gateway/dist/web/server.js +4 -1
  201. package/packages/gateway/dist/web/server.js.map +1 -1
  202. package/packages/gateway/dist/web/write.d.ts +14 -0
  203. package/packages/gateway/dist/web/write.d.ts.map +1 -1
  204. package/packages/gateway/dist/web/write.js +21 -0
  205. package/packages/gateway/dist/web/write.js.map +1 -1
  206. package/packages/install/dist/.tsbuildinfo +1 -1
  207. package/packages/install/dist/supervisor.d.ts.map +1 -1
  208. package/packages/install/dist/supervisor.js +1 -0
  209. package/packages/install/dist/supervisor.js.map +1 -1
  210. package/packages/install/dist/upgrade.d.ts +9 -0
  211. package/packages/install/dist/upgrade.d.ts.map +1 -1
  212. package/packages/install/dist/upgrade.js +20 -0
  213. package/packages/install/dist/upgrade.js.map +1 -1
  214. package/packages/runtime/dist/.tsbuildinfo +1 -1
  215. package/packages/runtime/dist/codex-direct.d.ts +1 -1
  216. package/packages/runtime/dist/codex-direct.js +1 -1
  217. package/packages/web/dist/assets/{index-Dz7zNZd9.js → index-BibqUZwt.js} +61 -61
  218. package/packages/web/dist/assets/{lottie_light-CDos62XP.js → lottie_light-D7CS-IeP.js} +1 -1
  219. package/packages/web/dist/build.json +1 -1
  220. package/packages/web/dist/index.html +1 -1
  221. package/packages/web/dist/precache.json +3 -3
@@ -65,13 +65,30 @@ async function fetch(connection, range, query, options) {
65
65
  };
66
66
  queryStructure.push(bodyPeek);
67
67
  };
68
- // IMAP fetch macros (ALL, FAST, FULL) and standard data items map directly to IMAP atoms
69
- ['all', 'fast', 'full', 'uid', 'flags', 'bodyStructure', 'envelope', 'internalDate'].forEach(key => {
70
- if (query[key]) {
68
+ // The ALL, FAST and FULL macros may only be sent on their own, never in a list with other
69
+ // items (RFC 3501 section 9), and UID is always in the list, so they are expanded into
70
+ // the items they stand for. FULL is expanded to BODYSTRUCTURE rather than the
71
+ // non-extensible BODY, as documented for the full option.
72
+ let full = !!query.full;
73
+ let all = !!query.all || full;
74
+ let fast = !!query.fast || all;
75
+ let items = {
76
+ flags: query.flags || fast,
77
+ internalDate: query.internalDate || fast,
78
+ size: query.size || fast,
79
+ envelope: query.envelope || all,
80
+ bodyStructure: query.bodyStructure || full
81
+ };
82
+ // standard data items map directly to IMAP atoms
83
+ if (query.uid) {
84
+ queryStructure.push({ type: 'ATOM', value: 'UID' });
85
+ }
86
+ ['flags', 'bodyStructure', 'envelope', 'internalDate'].forEach(key => {
87
+ if (items[key]) {
71
88
  queryStructure.push({ type: 'ATOM', value: key.toUpperCase() });
72
89
  }
73
90
  });
74
- if (query.size) {
91
+ if (items.size) {
75
92
  queryStructure.push({ type: 'ATOM', value: 'RFC822.SIZE' });
76
93
  }
77
94
  // Fetch full message source, optionally with byte range (start/maxLength)
@@ -194,7 +211,7 @@ async function fetch(connection, range, query, options) {
194
211
  if (consumerError) {
195
212
  return;
196
213
  }
197
- let formatted = await (0, tools_js_1.formatMessageResponse)(untagged, mailbox, connection.idHashAlgorithm);
214
+ let formatted = await (0, tools_js_1.formatMessageResponse)(untagged, mailbox, connection.idHashAlgorithm, connection);
198
215
  if (typeof options.onUntaggedFetch === 'function') {
199
216
  /* c8 ignore next */ // a UID FETCH row without its UID is a non-compliant server, so the seq fallback is not exercised
200
217
  let key = options.uid ? formatted.uid || formatted.seq : formatted.seq;
@@ -61,7 +61,10 @@ async function store(connection, range, flags, options) {
61
61
  const dropped = [];
62
62
  flags = (Array.isArray(flags) ? flags : [].concat(flags || []))
63
63
  .map(flag => {
64
- let formatted = (0, tools_js_1.formatFlag)(flag);
64
+ // Gmail labels other than the \-prefixed system labels are mailbox names: astrings in
65
+ // the form mailbox names take on the session (modified UTF-7 unless UTF-8 is enabled),
66
+ // not atoms like IMAP keywords
67
+ let formatted = options.useLabels && flag && flag.charAt(0) !== '\\' ? (0, tools_js_1.encodePath)(connection, flag) : (0, tools_js_1.formatFlag)(flag);
65
68
  if (!formatted || (!(0, tools_js_1.canUseFlag)(flagSource, formatted) && operationName !== 'remove')) {
66
69
  dropped.push(flag);
67
70
  return false;
@@ -83,13 +86,10 @@ async function store(connection, range, flags, options) {
83
86
  if (!flags.length && !clearAll) {
84
87
  return false;
85
88
  }
86
- let attributes = [
87
- { type: 'SEQUENCE', value: range },
88
- { type: 'ATOM', value: operation },
89
- flags.map(flag => ({ type: 'ATOM', value: flag }))
90
- ];
89
+ let attributes = [{ type: 'SEQUENCE', value: range }];
91
90
  // CONDSTORE (RFC 7162): UNCHANGEDSINCE modifier prevents updating messages whose
92
91
  // mod-sequence is higher than the specified value, avoiding overwriting concurrent changes.
92
+ // The store-modifiers list goes between the sequence set and the item name (section 3.1.3).
93
93
  if (options.unchangedSince && connection.enabled.has('CONDSTORE') && !mailbox.noModseq) {
94
94
  attributes.push([
95
95
  {
@@ -102,6 +102,7 @@ async function store(connection, range, flags, options) {
102
102
  }
103
103
  ]);
104
104
  }
105
+ attributes.push({ type: 'ATOM', value: operation }, flags.map(flag => ({ type: 'ATOM', value: flag })));
105
106
  let response;
106
107
  try {
107
108
  response = await connection.exec(options.uid ? 'UID STORE' : 'STORE', attributes);
@@ -19,6 +19,113 @@ const limited_passthrough_js_1 = require("./limited-passthrough.js");
19
19
  // away (a consumer destroying it closes it without a 'drain')
20
20
  const DRAIN_WAIT_EVENTS = ['drain', 'error', 'close'];
21
21
  const tools_js_1 = require("./tools.js");
22
+ const isEmptySection = (value) => !value?.length;
23
+ /**
24
+ * The section to ask again when exactly one of a part's MIME headers and content came back
25
+ * empty (see refetchDroppedSections()), undefined when both or neither did
26
+ */
27
+ const droppedSection = (mime, content, mimeRequest, contentRequest) => {
28
+ if (isEmptySection(mime) === isEmptySection(content)) {
29
+ return undefined;
30
+ }
31
+ return isEmptySection(content) ? contentRequest : mimeRequest;
32
+ };
33
+ /** Start offset of every partial section among the requests, keyed by section */
34
+ const partialStarts = (requests) => {
35
+ let starts = new Map();
36
+ for (let request of requests) {
37
+ if (typeof request !== 'string') {
38
+ starts.set(request.key, Number(request.start) || 0);
39
+ }
40
+ }
41
+ return starts;
42
+ };
43
+ // How many times a request is repeated after answers that belong to another request
44
+ const MAX_FOREIGN_ANSWERS = 3;
45
+ const requestedUid = (range, options) => options.uid && /^\d+$/.test(String(range)) ? Number(range) : undefined;
46
+ const isForeignAnswer = (response, expected) => {
47
+ if (expected.uid && response.uid && response.uid !== expected.uid) {
48
+ return true;
49
+ }
50
+ for (let [key, start] of expected.origins) {
51
+ let origin = response.partialOrigins && response.partialOrigins.get(key);
52
+ // An answer without the origin is taken as is: some servers leave it out, and some
53
+ // ignore the partial specifier altogether
54
+ if (typeof origin === 'number' && origin !== start) {
55
+ return true;
56
+ }
57
+ }
58
+ return false;
59
+ };
60
+ /**
61
+ * fetchOne() that takes only an answer belonging to the request. Apache James now and then
62
+ * writes the head of a FETCH answer after its tagged OK, so the data of one request shows up
63
+ * within the answer to the next one. Taken at face value it would be the data of that next
64
+ * request, and a download would end without an error but with misplaced bytes. An answer for
65
+ * another UID, or with a partial section that starts at another offset than asked, is dropped
66
+ * and the request repeated.
67
+ */
68
+ async function fetchExpected(client, range, query, options, expected) {
69
+ for (let attempt = 1;; attempt++) {
70
+ let response = await client.fetchOne(range, query, options);
71
+ if (!response || !isForeignAnswer(response, expected)) {
72
+ return response;
73
+ }
74
+ client.log.warn({
75
+ msg: 'Server answered with data of another request, asking again',
76
+ uid: response.uid,
77
+ origins: response.partialOrigins && Object.fromEntries(response.partialOrigins),
78
+ attempt,
79
+ cid: client.id
80
+ });
81
+ if (attempt >= MAX_FOREIGN_ANSWERS) {
82
+ let err = new Error('Server kept answering with data of another request');
83
+ err.code = 'DownloadIncomplete';
84
+ err.cid = client.id;
85
+ throw err;
86
+ }
87
+ }
88
+ }
89
+ /**
90
+ * Apache James (and the servers built on it, Twake Mail among them) answers only the first
91
+ * section it is asked for each MIME part of one FETCH and returns the other one empty:
92
+ * BODY[2.MIME] with BODY[2] yields the headers and a zero-length body, the reverse order loses
93
+ * the headers (FetchGroup.addPartContent() keeps the first descriptor for a part path). Asks the
94
+ * sections that came back empty again, in a FETCH of their own, and merges the answer into
95
+ * `response`. Callers only list a section whose companion did arrive, so a compliant server
96
+ * pays the extra round trip only for a part that really is empty.
97
+ */
98
+ async function refetchDroppedSections(client, response, range, options, sections) {
99
+ if (!sections.length) {
100
+ return;
101
+ }
102
+ client.log.debug({
103
+ msg: 'Server answered a body section empty while its companion section was not, asking it again separately',
104
+ sections: sections.map(section => (typeof section === 'string' ? section : section.key)),
105
+ cid: client.id
106
+ });
107
+ // the UID pins the message even when the first command addressed it by sequence number
108
+ let uid = response.uid;
109
+ let retry = await fetchExpected(client, uid || range, { uid: true, bodyParts: sections }, uid ? { ...options, uid: true } : options, {
110
+ uid,
111
+ origins: partialStarts(sections)
112
+ });
113
+ if (!retry) {
114
+ return;
115
+ }
116
+ if (retry.headers) {
117
+ response.headers = retry.headers;
118
+ }
119
+ for (let [key, value] of retry.bodyParts || []) {
120
+ (response.bodyParts ??= new Map()).set(key, value);
121
+ if (retry.binaryParts && retry.binaryParts.has(key)) {
122
+ (response.binaryParts ??= new Set()).add(key);
123
+ }
124
+ else if (response.binaryParts) {
125
+ response.binaryParts.delete(key);
126
+ }
127
+ }
128
+ }
22
129
  /**
23
130
  * Implements ImapFlow.download(), see its documentation
24
131
  *
@@ -66,6 +173,7 @@ async function downloadMessage(client, range, part, options) {
66
173
  let getNextPart = async (query) => {
67
174
  query = query || {};
68
175
  let mimeKey;
176
+ let contentRequest;
69
177
  if (!part) {
70
178
  query.source = {
71
179
  start: processed,
@@ -88,13 +196,18 @@ async function downloadMessage(client, range, part, options) {
88
196
  query.bodyParts.push(mimeKey);
89
197
  }
90
198
  }
91
- query.bodyParts.push({
199
+ contentRequest = {
92
200
  key: part,
93
201
  start: processed,
94
202
  maxLength: chunkSize
95
- });
203
+ };
204
+ query.bodyParts.push(contentRequest);
96
205
  }
97
- let response = await client.fetchOne(range, query, downloadOptions);
206
+ let expected = {
207
+ uid: uid || requestedUid(range, downloadOptions),
208
+ origins: new Map([[part || '', processed]])
209
+ };
210
+ let response = await fetchExpected(client, range, query, downloadOptions, expected);
98
211
  if (!response) {
99
212
  return { response: false, chunk: false };
100
213
  }
@@ -104,6 +217,12 @@ async function downloadMessage(client, range, part, options) {
104
217
  range = uid;
105
218
  downloadOptions.uid = true;
106
219
  }
220
+ if (mimeKey && contentRequest) {
221
+ let dropped = droppedSection(mimeKey === 'header' ? response.headers : response.bodyParts?.get(mimeKey), response.bodyParts?.get(part), mimeKey, contentRequest);
222
+ if (dropped) {
223
+ await refetchDroppedSections(client, response, range, downloadOptions, [dropped]);
224
+ }
225
+ }
107
226
  let chunk = !part ? response.source : response.bodyParts && response.bodyParts.get(part);
108
227
  if (!chunk) {
109
228
  return {};
@@ -458,16 +577,31 @@ async function downloadMessageParts(client, range, parts, options) {
458
577
  // again on the answer for servers that ignore the partial specifier
459
578
  let maxBytes = (0, limited_passthrough_js_1.normalizeByteLimit)(downloadOptions.maxBytes);
460
579
  let query = { bodyParts: [] };
580
+ let contentRequests = new Map();
461
581
  for (let part of parts) {
462
582
  query.bodyParts.push(part + '.mime');
463
583
  // The partial specifier carries a 32-bit length (RFC 9051 "number"), so a cap beyond
464
584
  // that is applied on the answer alone
465
- query.bodyParts.push(maxBytes > 0xffffffff ? part : { key: part, start: 0, maxLength: maxBytes });
585
+ let contentRequest = maxBytes > 0xffffffff ? part : { key: part, start: 0, maxLength: maxBytes };
586
+ contentRequests.set(part, contentRequest);
587
+ query.bodyParts.push(contentRequest);
466
588
  }
467
- let response = await client.fetchOne(range, query, downloadOptions);
589
+ let response = await fetchExpected(client, range, query, downloadOptions, {
590
+ uid: requestedUid(range, downloadOptions),
591
+ origins: partialStarts(query.bodyParts)
592
+ });
468
593
  if (!response || !response.bodyParts) {
469
594
  return {};
470
595
  }
596
+ let dropped = [];
597
+ for (let [part, contentRequest] of contentRequests) {
598
+ let section = droppedSection(response.bodyParts.get(part + '.mime'), response.bodyParts.get(part), part + '.mime', contentRequest);
599
+ if (section) {
600
+ dropped.push(section);
601
+ }
602
+ }
603
+ // Sections of different parts do not collide, so every dropped one fits in one FETCH
604
+ await refetchDroppedSections(client, response, range, downloadOptions, dropped);
471
605
  let data = {};
472
606
  for (let [part, content] of response.bodyParts) {
473
607
  let keyParts = part.split('.mime');
@@ -122,8 +122,8 @@ export declare class ImapFlow extends EventEmitter {
122
122
  */
123
123
  usable: boolean;
124
124
  /**
125
- * Currently authenticated user or `false` if mailbox is not open
126
- * or `true` if connection was authenticated by PREAUTH
125
+ * `true` once the connection is authenticated (by LOGIN, AUTHENTICATE or a PREAUTH greeting),
126
+ * `false` before that. The user name is in `options.auth.user`
127
127
  */
128
128
  authenticated: string | boolean;
129
129
  /**
@@ -1500,9 +1500,8 @@ class ImapFlow extends node_events_1.EventEmitter {
1500
1500
  // of the IP - accepting any "localhost" certificate for any IP-hosted
1501
1501
  // server, and rejecting legitimate IP-SAN certificates.
1502
1502
  host: this.host,
1503
- servername: this.servername,
1504
1503
  port: this.port
1505
- }, this.options.tls || {});
1504
+ }, this.tlsServername(), this.options.tls || {});
1506
1505
  this.clearSocketHandlers();
1507
1506
  let settled = false;
1508
1507
  // Single settlement path for the upgrade. Every terminal outcome - handshake
@@ -1892,12 +1891,18 @@ class ImapFlow extends node_events_1.EventEmitter {
1892
1891
  uids = untagged.attributes[0].value;
1893
1892
  }
1894
1893
  let uidList = (0, tools_js_1.expandRange)(uids);
1894
+ let earlier = tags.includes('EARLIER');
1895
+ // RFC 7162 section 3.2.10: unlike VANISHED (EARLIER), a plain VANISHED reports messages the
1896
+ // client knows about and decrements the message count like the same number of EXPUNGEs would
1897
+ if (!earlier) {
1898
+ mailbox.exists = Math.max(0, mailbox.exists - uidList.length);
1899
+ }
1895
1900
  for (let uid of uidList) {
1896
1901
  let payload = {
1897
1902
  path: mailbox.path,
1898
1903
  uid,
1899
1904
  vanished: true,
1900
- earlier: tags.includes('EARLIER')
1905
+ earlier
1901
1906
  };
1902
1907
  await this.notifyExpunge(payload);
1903
1908
  }
@@ -1909,7 +1914,7 @@ class ImapFlow extends node_events_1.EventEmitter {
1909
1914
  // mailbox closed, ignore
1910
1915
  return;
1911
1916
  }
1912
- let message = await (0, tools_js_1.formatMessageResponse)(untagged, mailbox, this.idHashAlgorithm);
1917
+ let message = await (0, tools_js_1.formatMessageResponse)(untagged, mailbox, this.idHashAlgorithm, this);
1913
1918
  if (message.flags) {
1914
1919
  let updateEvent = {
1915
1920
  path: mailbox.path,
@@ -1938,6 +1943,12 @@ class ImapFlow extends node_events_1.EventEmitter {
1938
1943
  }
1939
1944
  return true;
1940
1945
  }
1946
+ // servername for tls.connect(), left out for an IP literal host (this.servername is false then):
1947
+ // Node treats a false value like a missing one, but Bun throws a TypeError for it
1948
+ /** @internal */
1949
+ tlsServername() {
1950
+ return this.servername ? { servername: this.servername } : {};
1951
+ }
1941
1952
  // Normalizes a message range from various input formats into an IMAP-compatible
1942
1953
  // sequence string (e.g., "1:5,7,10:*"). Handles: numbers, "*", {all:true},
1943
1954
  // {uid:value}, search query objects (resolved via SEARCH), and arrays of numbers.
@@ -1980,6 +1991,11 @@ class ImapFlow extends node_events_1.EventEmitter {
1980
1991
  if (!value) {
1981
1992
  return false;
1982
1993
  }
1994
+ // An empty mailbox has no message numbers: every sequence set, "1:*" included, would get a
1995
+ // BAD (RFC 9051 section 9, seq-number). UID sets may point past the end, so they are sent.
1996
+ if (!options.uid && this.mailbox && !this.mailbox.exists) {
1997
+ return false;
1998
+ }
1983
1999
  return value;
1984
2000
  }
1985
2001
  // The single definition of "the connection is not free". A held or queued mailbox lock, a
@@ -2049,9 +2065,8 @@ class ImapFlow extends node_events_1.EventEmitter {
2049
2065
  let connector = this.secureConnection ? node_tls_1.default : node_net_1.default;
2050
2066
  let opts = Object.assign({
2051
2067
  host: this.host,
2052
- servername: this.servername,
2053
2068
  port: this.port
2054
- }, this.options.tls || {});
2069
+ }, this.tlsServername(), this.options.tls || {});
2055
2070
  this.untaggedHandlers.OK = (...args) => this.initialOK(...args);
2056
2071
  this.untaggedHandlers.BYE = (...args) => this.serverBye(...args);
2057
2072
  this.untaggedHandlers.PREAUTH = () => this.initialPREAUTH();
@@ -1,3 +1,3 @@
1
1
  export declare const name = "imapflow";
2
- export declare const version = "2.2.7";
2
+ export declare const version = "2.2.8";
3
3
  export declare const homepage = "https://imapflow.com/";
@@ -3,5 +3,5 @@
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.homepage = exports.version = exports.name = void 0;
5
5
  exports.name = "imapflow";
6
- exports.version = "2.2.7";
6
+ exports.version = "2.2.8";
7
7
  exports.homepage = "https://imapflow.com/";
@@ -371,10 +371,13 @@ const searchCompiler = (connection, query) => {
371
371
  fail('InvalidSearchQuery', `Search value for ${term.toLowerCase()} must be a string`);
372
372
  }
373
373
  let flag = (0, tools_js_1.formatFlag)(params[term]);
374
- // formatFlag() refuses \Recent, which is not a keyword. Dropping the
375
- // criterion would widen the search, so the query is refused instead
374
+ // formatFlag() refuses \Recent, which is not a keyword, and values that are
375
+ // not atoms. Dropping the criterion would widen the search, so the query is
376
+ // refused instead
376
377
  if (flag === false) {
377
- fail('InvalidSearchQuery', `${params[term]} can not be searched as a keyword, use the "recent" search key instead`);
378
+ fail('InvalidSearchQuery', /^\\recent$/i.test(params[term])
379
+ ? `${params[term]} can not be searched as a keyword, use the "recent" search key instead`
380
+ : `${params[term]} is not a valid keyword`);
378
381
  }
379
382
  // Compiled even when the mailbox does not allow the keyword: the
380
383
  // correct answer is then the empty set, which dropping the
@@ -293,9 +293,10 @@ export declare function getColorFlags(color: string | null | undefined): {
293
293
  * @param untagged - Parsed untagged IMAP response
294
294
  * @param mailbox - Current mailbox state object
295
295
  * @param idHashAlgorithm - Hash for the fallback message id, `md5` unless the client was told otherwise
296
+ * @param connection - Connection the response arrived on, decodes Gmail labels like mailbox names
296
297
  * @returns Formatted message object with properties like seq, uid, flags, envelope, etc.
297
298
  */
298
- export declare function formatMessageResponse(untagged: ImapResponse, mailbox: MailboxObject, idHashAlgorithm?: string): Promise<FetchMessageObject>;
299
+ export declare function formatMessageResponse(untagged: ImapResponse, mailbox: MailboxObject, idHashAlgorithm?: string, connection?: ImapFlow): Promise<FetchMessageObject>;
299
300
  /**
300
301
  * Strips surrounding double quotes from a name string.
301
302
  *
@@ -364,8 +365,8 @@ export declare function formatDate(value: unknown): string | undefined;
364
365
  */
365
366
  export declare function formatDateTime(value: unknown): string | undefined;
366
367
  /**
367
- * Normalizes a flag string. Returns false for non-settable flags (e.g. \Recent),
368
- * and capitalizes system flags properly.
368
+ * Normalizes a flag string. Returns false for non-settable flags (e.g. \Recent) and for
369
+ * values that are not valid flags (keywords must be atoms), and capitalizes system flags properly.
369
370
  *
370
371
  * @param flag - Flag string to normalize
371
372
  * @returns Normalized flag string, or false if the flag cannot be set
@@ -56,6 +56,7 @@ exports.packMessageRange = packMessageRange;
56
56
  const libmime_1 = __importDefault(require("libmime"));
57
57
  const charsets_js_1 = require("./charsets.js");
58
58
  const imap_handler_js_1 = require("./handler/imap-handler.js");
59
+ const imap_formal_syntax_js_1 = __importDefault(require("./handler/imap-formal-syntax.js"));
59
60
  const node_crypto_1 = require("node:crypto");
60
61
  const jp_decoder_js_1 = require("./jp-decoder.js");
61
62
  const iconv_lite_1 = __importDefault(require("iconv-lite"));
@@ -336,7 +337,8 @@ function buildStatusQueryAttributes(connection, statusQuery) {
336
337
  }
337
338
  break;
338
339
  case 'HIGHESTMODSEQ':
339
- if (connection.capabilities.has('CONDSTORE')) {
340
+ // QRESYNC implies CONDSTORE (RFC 7162 3.2.3)
341
+ if (connection.capabilities.has('CONDSTORE') || connection.capabilities.has('QRESYNC')) {
340
342
  attributes.push({ type: 'ATOM', value: key.toUpperCase() });
341
343
  }
342
344
  break;
@@ -717,24 +719,40 @@ function getColorFlags(color) {
717
719
  * @param untagged - Parsed untagged IMAP response
718
720
  * @param mailbox - Current mailbox state object
719
721
  * @param idHashAlgorithm - Hash for the fallback message id, `md5` unless the client was told otherwise
722
+ * @param connection - Connection the response arrived on, decodes Gmail labels like mailbox names
720
723
  * @returns Formatted message object with properties like seq, uid, flags, envelope, etc.
721
724
  */
722
- async function formatMessageResponse(untagged, mailbox, idHashAlgorithm) {
725
+ async function formatMessageResponse(untagged, mailbox, idHashAlgorithm, connection) {
723
726
  let map = {};
724
727
  // The sequence number indexes into mailbox state, so an unusable one is dropped rather
725
728
  // than coerced to NaN or Infinity
726
729
  map.seq = parseUintValue(untagged.command, exports.MAX_UINT32_DIGITS) || undefined;
727
730
  let key;
731
+ // the <origin> of a partial section ("BODY[2]<1024>"), kept for the value that follows
732
+ let origin = false;
733
+ let partialOrigins;
734
+ let recordOrigin = (sectionKey) => {
735
+ if (origin !== false) {
736
+ if (!partialOrigins) {
737
+ partialOrigins = new Map();
738
+ }
739
+ partialOrigins.set(sectionKey, origin);
740
+ }
741
+ };
728
742
  let attributes = ((untagged.attributes && untagged.attributes[1]) || []);
729
743
  for (let i = 0, len = attributes.length; i < len; i++) {
730
744
  let attribute = attributes[i];
731
745
  if (i % 2 === 0) {
746
+ origin = false;
732
747
  key = (await (0, imap_handler_js_1.compiler)({
733
748
  attributes: [attribute]
734
749
  }))
735
750
  .toString()
736
751
  .toLowerCase()
737
- .replace(/<\d+(\.\d+)?>$/, '');
752
+ .replace(/<(\d+)(\.\d+)?>$/, (match, start) => {
753
+ origin = parseUintValue(start, exports.MAX_UINT32_DIGITS);
754
+ return '';
755
+ });
738
756
  continue;
739
757
  }
740
758
  /* c8 ignore start */ // defensive: key is always a string produced by the compiler above
@@ -779,6 +797,7 @@ async function formatMessageResponse(untagged, mailbox, idHashAlgorithm) {
779
797
  case 'body[]':
780
798
  case 'binary[]':
781
799
  map.source = getBuffer(attribute);
800
+ recordOrigin('');
782
801
  break;
783
802
  case 'uid':
784
803
  // A UID feeds mailbox.uidNext one line below, and from there every range
@@ -825,7 +844,8 @@ async function formatMessageResponse(untagged, mailbox, idHashAlgorithm) {
825
844
  map.threadId = getString(attribute);
826
845
  break;
827
846
  case 'x-gm-labels':
828
- map.labels = new Set(getArray(attribute));
847
+ // labels are mailbox names, modified UTF-7 unless UTF-8 is enabled
848
+ map.labels = new Set(getArray(attribute).map(label => (connection ? decodePath(connection, label) : label)));
829
849
  break;
830
850
  case 'rfc822.size':
831
851
  map.size = getUint(attribute) || 0;
@@ -871,6 +891,7 @@ async function formatMessageResponse(untagged, mailbox, idHashAlgorithm) {
871
891
  map.bodyParts = new Map();
872
892
  }
873
893
  map.bodyParts.set(partKey, value);
894
+ recordOrigin(partKey);
874
895
  if (match[1].toLowerCase() === 'binary') {
875
896
  // The part arrived via FETCH BINARY (RFC 3516, FETCH side folded
876
897
  // into IMAP4rev2), so the server has already removed the
@@ -912,6 +933,10 @@ async function formatMessageResponse(untagged, mailbox, idHashAlgorithm) {
912
933
  .update([path, mailbox.uidValidity?.toString() || '', map.uid.toString()].join(':'))
913
934
  .digest('hex');
914
935
  }
936
+ if (partialOrigins) {
937
+ // non-enumerable, so it stays out of logged and serialized fetch results
938
+ Object.defineProperty(map, 'partialOrigins', { value: partialOrigins, writable: true, configurable: true });
939
+ }
915
940
  if (map.flags) {
916
941
  let flagColor = getFlagColor(map.flags);
917
942
  if (flagColor) {
@@ -1354,14 +1379,22 @@ function formatDateTime(value) {
1354
1379
  let timeStr = date.toISOString().substring(11, 19);
1355
1380
  return `${dateStr} ${timeStr} +0000`;
1356
1381
  }
1382
+ // the memoized ATOM-CHAR set of RFC 9051 section 9
1383
+ const atomChars = imap_formal_syntax_js_1.default['ATOM-CHAR'];
1357
1384
  /**
1358
- * Normalizes a flag string. Returns false for non-settable flags (e.g. \Recent),
1359
- * and capitalizes system flags properly.
1385
+ * Normalizes a flag string. Returns false for non-settable flags (e.g. \Recent) and for
1386
+ * values that are not valid flags (keywords must be atoms), and capitalizes system flags properly.
1360
1387
  *
1361
1388
  * @param flag - Flag string to normalize
1362
1389
  * @returns Normalized flag string, or false if the flag cannot be set
1363
1390
  */
1364
1391
  function formatFlag(flag) {
1392
+ // RFC 9051 section 9: flag-keyword is an atom and flag-extension is "\\" atom, the same check
1393
+ // the compiler uses to decide what it can send unquoted
1394
+ let atom = flag.charAt(0) === '\\' ? flag.slice(1) : flag;
1395
+ if (!atom || imap_formal_syntax_js_1.default.verify(atom, atomChars()) >= 0) {
1396
+ return false;
1397
+ }
1365
1398
  switch (flag.toLowerCase()) {
1366
1399
  case '\\recent':
1367
1400
  // can not set or remove
@@ -63,15 +63,26 @@ async function authOauth(connection, username, accessToken) {
63
63
  // Empty breaker: XOAUTH2 expects an empty response to abort the SASL exchange
64
64
  breaker = '';
65
65
  }
66
+ let encoded = Buffer.from(oauthbearer).toString('base64');
67
+ // Without SASL-IR the payload may not ride on the command line (RFC 4959 section 3): it is
68
+ // sent as the answer to the first, empty continuation request instead
69
+ let payloadPending = !connection.capabilities.has('SASL-IR');
66
70
  let errorResponse = false;
67
71
  try {
68
- let response = await connection.exec('AUTHENTICATE', [
69
- { type: 'ATOM', value: command },
70
- { type: 'ATOM', value: Buffer.from(oauthbearer).toString('base64'), sensitive: true }
71
- ], {
72
+ let attributes = [{ type: 'ATOM', value: command }];
73
+ if (!payloadPending) {
74
+ attributes.push({ type: 'ATOM', value: encoded, sensitive: true });
75
+ }
76
+ let response = await connection.exec('AUTHENTICATE', attributes, {
72
77
  // Server sends a "+" continuation if auth fails, with a base64 JSON error payload.
73
78
  // We decode it for diagnostics, then send the breaker to terminate the exchange.
74
79
  onPlusTag: async (resp) => {
80
+ if (payloadPending) {
81
+ payloadPending = false;
82
+ connection.log.debug({ src: 'c', msg: '(* value hidden *)', comment: `Encoded response for AUTH=${command}`, cid: connection.id });
83
+ connection.write(encoded);
84
+ return;
85
+ }
75
86
  if (resp.attributes && resp.attributes[0] && resp.attributes[0].type === 'TEXT') {
76
87
  try {
77
88
  errorResponse = JSON.parse(Buffer.from(resp.attributes[0].value, 'base64').toString());
@@ -46,6 +46,12 @@ export default async function enable(connection, extensionList) {
46
46
  // extensions enabled by this command (RFC 5161), so a replace would drop
47
47
  // grants from an earlier ENABLE call
48
48
  connection.enabled = new Set([...connection.enabled, ...enabled]);
49
+ if (connection.enabled.has('QRESYNC')) {
50
+ // ENABLE QRESYNC is a CONDSTORE enabling command (RFC 7162 3.2.3), whether or not the
51
+ // server lists CONDSTORE in its ENABLED answer. Apache James advertises only QRESYNC
52
+ // and leaves a lone ENABLE CONDSTORE unanswered, which the RFC allows.
53
+ connection.enabled.add('CONDSTORE');
54
+ }
49
55
  response.next();
50
56
  return connection.enabled;
51
57
  }
@@ -62,13 +62,30 @@ export default async function fetch(connection, range, query, options) {
62
62
  };
63
63
  queryStructure.push(bodyPeek);
64
64
  };
65
- // IMAP fetch macros (ALL, FAST, FULL) and standard data items map directly to IMAP atoms
66
- ['all', 'fast', 'full', 'uid', 'flags', 'bodyStructure', 'envelope', 'internalDate'].forEach(key => {
67
- if (query[key]) {
65
+ // The ALL, FAST and FULL macros may only be sent on their own, never in a list with other
66
+ // items (RFC 3501 section 9), and UID is always in the list, so they are expanded into
67
+ // the items they stand for. FULL is expanded to BODYSTRUCTURE rather than the
68
+ // non-extensible BODY, as documented for the full option.
69
+ let full = !!query.full;
70
+ let all = !!query.all || full;
71
+ let fast = !!query.fast || all;
72
+ let items = {
73
+ flags: query.flags || fast,
74
+ internalDate: query.internalDate || fast,
75
+ size: query.size || fast,
76
+ envelope: query.envelope || all,
77
+ bodyStructure: query.bodyStructure || full
78
+ };
79
+ // standard data items map directly to IMAP atoms
80
+ if (query.uid) {
81
+ queryStructure.push({ type: 'ATOM', value: 'UID' });
82
+ }
83
+ ['flags', 'bodyStructure', 'envelope', 'internalDate'].forEach(key => {
84
+ if (items[key]) {
68
85
  queryStructure.push({ type: 'ATOM', value: key.toUpperCase() });
69
86
  }
70
87
  });
71
- if (query.size) {
88
+ if (items.size) {
72
89
  queryStructure.push({ type: 'ATOM', value: 'RFC822.SIZE' });
73
90
  }
74
91
  // Fetch full message source, optionally with byte range (start/maxLength)
@@ -191,7 +208,7 @@ export default async function fetch(connection, range, query, options) {
191
208
  if (consumerError) {
192
209
  return;
193
210
  }
194
- let formatted = await formatMessageResponse(untagged, mailbox, connection.idHashAlgorithm);
211
+ let formatted = await formatMessageResponse(untagged, mailbox, connection.idHashAlgorithm, connection);
195
212
  if (typeof options.onUntaggedFetch === 'function') {
196
213
  /* c8 ignore next */ // a UID FETCH row without its UID is a non-compliant server, so the seq fallback is not exercised
197
214
  let key = options.uid ? formatted.uid || formatted.seq : formatted.seq;