@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
@@ -1,4 +1,4 @@
1
- import { formatFlag, canUseFlag, reportCommandError, getSelectedMailbox } from '../tools.js';
1
+ import { formatFlag, canUseFlag, encodePath, reportCommandError, getSelectedMailbox } from '../tools.js';
2
2
  /**
3
3
  * Updates flags or labels for messages in the selected mailbox.
4
4
  *
@@ -58,7 +58,10 @@ export default async function store(connection, range, flags, options) {
58
58
  const dropped = [];
59
59
  flags = (Array.isArray(flags) ? flags : [].concat(flags || []))
60
60
  .map(flag => {
61
- let formatted = formatFlag(flag);
61
+ // Gmail labels other than the \-prefixed system labels are mailbox names: astrings in
62
+ // the form mailbox names take on the session (modified UTF-7 unless UTF-8 is enabled),
63
+ // not atoms like IMAP keywords
64
+ let formatted = options.useLabels && flag && flag.charAt(0) !== '\\' ? encodePath(connection, flag) : formatFlag(flag);
62
65
  if (!formatted || (!canUseFlag(flagSource, formatted) && operationName !== 'remove')) {
63
66
  dropped.push(flag);
64
67
  return false;
@@ -80,13 +83,10 @@ export default async function store(connection, range, flags, options) {
80
83
  if (!flags.length && !clearAll) {
81
84
  return false;
82
85
  }
83
- let attributes = [
84
- { type: 'SEQUENCE', value: range },
85
- { type: 'ATOM', value: operation },
86
- flags.map(flag => ({ type: 'ATOM', value: flag }))
87
- ];
86
+ let attributes = [{ type: 'SEQUENCE', value: range }];
88
87
  // CONDSTORE (RFC 7162): UNCHANGEDSINCE modifier prevents updating messages whose
89
88
  // mod-sequence is higher than the specified value, avoiding overwriting concurrent changes.
89
+ // The store-modifiers list goes between the sequence set and the item name (section 3.1.3).
90
90
  if (options.unchangedSince && connection.enabled.has('CONDSTORE') && !mailbox.noModseq) {
91
91
  attributes.push([
92
92
  {
@@ -99,6 +99,7 @@ export default async function store(connection, range, flags, options) {
99
99
  }
100
100
  ]);
101
101
  }
102
+ attributes.push({ type: 'ATOM', value: operation }, flags.map(flag => ({ type: 'ATOM', value: flag })));
102
103
  let response;
103
104
  try {
104
105
  response = await connection.exec(options.uid ? 'UID STORE' : 'STORE', attributes);
@@ -12,6 +12,113 @@ import { LimitedPassthrough, normalizeByteLimit } from './limited-passthrough.js
12
12
  // away (a consumer destroying it closes it without a 'drain')
13
13
  const DRAIN_WAIT_EVENTS = ['drain', 'error', 'close'];
14
14
  import { getDecoder, isUnsafeKey } from './tools.js';
15
+ const isEmptySection = (value) => !value?.length;
16
+ /**
17
+ * The section to ask again when exactly one of a part's MIME headers and content came back
18
+ * empty (see refetchDroppedSections()), undefined when both or neither did
19
+ */
20
+ const droppedSection = (mime, content, mimeRequest, contentRequest) => {
21
+ if (isEmptySection(mime) === isEmptySection(content)) {
22
+ return undefined;
23
+ }
24
+ return isEmptySection(content) ? contentRequest : mimeRequest;
25
+ };
26
+ /** Start offset of every partial section among the requests, keyed by section */
27
+ const partialStarts = (requests) => {
28
+ let starts = new Map();
29
+ for (let request of requests) {
30
+ if (typeof request !== 'string') {
31
+ starts.set(request.key, Number(request.start) || 0);
32
+ }
33
+ }
34
+ return starts;
35
+ };
36
+ // How many times a request is repeated after answers that belong to another request
37
+ const MAX_FOREIGN_ANSWERS = 3;
38
+ const requestedUid = (range, options) => options.uid && /^\d+$/.test(String(range)) ? Number(range) : undefined;
39
+ const isForeignAnswer = (response, expected) => {
40
+ if (expected.uid && response.uid && response.uid !== expected.uid) {
41
+ return true;
42
+ }
43
+ for (let [key, start] of expected.origins) {
44
+ let origin = response.partialOrigins && response.partialOrigins.get(key);
45
+ // An answer without the origin is taken as is: some servers leave it out, and some
46
+ // ignore the partial specifier altogether
47
+ if (typeof origin === 'number' && origin !== start) {
48
+ return true;
49
+ }
50
+ }
51
+ return false;
52
+ };
53
+ /**
54
+ * fetchOne() that takes only an answer belonging to the request. Apache James now and then
55
+ * writes the head of a FETCH answer after its tagged OK, so the data of one request shows up
56
+ * within the answer to the next one. Taken at face value it would be the data of that next
57
+ * request, and a download would end without an error but with misplaced bytes. An answer for
58
+ * another UID, or with a partial section that starts at another offset than asked, is dropped
59
+ * and the request repeated.
60
+ */
61
+ async function fetchExpected(client, range, query, options, expected) {
62
+ for (let attempt = 1;; attempt++) {
63
+ let response = await client.fetchOne(range, query, options);
64
+ if (!response || !isForeignAnswer(response, expected)) {
65
+ return response;
66
+ }
67
+ client.log.warn({
68
+ msg: 'Server answered with data of another request, asking again',
69
+ uid: response.uid,
70
+ origins: response.partialOrigins && Object.fromEntries(response.partialOrigins),
71
+ attempt,
72
+ cid: client.id
73
+ });
74
+ if (attempt >= MAX_FOREIGN_ANSWERS) {
75
+ let err = new Error('Server kept answering with data of another request');
76
+ err.code = 'DownloadIncomplete';
77
+ err.cid = client.id;
78
+ throw err;
79
+ }
80
+ }
81
+ }
82
+ /**
83
+ * Apache James (and the servers built on it, Twake Mail among them) answers only the first
84
+ * section it is asked for each MIME part of one FETCH and returns the other one empty:
85
+ * BODY[2.MIME] with BODY[2] yields the headers and a zero-length body, the reverse order loses
86
+ * the headers (FetchGroup.addPartContent() keeps the first descriptor for a part path). Asks the
87
+ * sections that came back empty again, in a FETCH of their own, and merges the answer into
88
+ * `response`. Callers only list a section whose companion did arrive, so a compliant server
89
+ * pays the extra round trip only for a part that really is empty.
90
+ */
91
+ async function refetchDroppedSections(client, response, range, options, sections) {
92
+ if (!sections.length) {
93
+ return;
94
+ }
95
+ client.log.debug({
96
+ msg: 'Server answered a body section empty while its companion section was not, asking it again separately',
97
+ sections: sections.map(section => (typeof section === 'string' ? section : section.key)),
98
+ cid: client.id
99
+ });
100
+ // the UID pins the message even when the first command addressed it by sequence number
101
+ let uid = response.uid;
102
+ let retry = await fetchExpected(client, uid || range, { uid: true, bodyParts: sections }, uid ? { ...options, uid: true } : options, {
103
+ uid,
104
+ origins: partialStarts(sections)
105
+ });
106
+ if (!retry) {
107
+ return;
108
+ }
109
+ if (retry.headers) {
110
+ response.headers = retry.headers;
111
+ }
112
+ for (let [key, value] of retry.bodyParts || []) {
113
+ (response.bodyParts ??= new Map()).set(key, value);
114
+ if (retry.binaryParts && retry.binaryParts.has(key)) {
115
+ (response.binaryParts ??= new Set()).add(key);
116
+ }
117
+ else if (response.binaryParts) {
118
+ response.binaryParts.delete(key);
119
+ }
120
+ }
121
+ }
15
122
  /**
16
123
  * Implements ImapFlow.download(), see its documentation
17
124
  *
@@ -59,6 +166,7 @@ export async function downloadMessage(client, range, part, options) {
59
166
  let getNextPart = async (query) => {
60
167
  query = query || {};
61
168
  let mimeKey;
169
+ let contentRequest;
62
170
  if (!part) {
63
171
  query.source = {
64
172
  start: processed,
@@ -81,13 +189,18 @@ export async function downloadMessage(client, range, part, options) {
81
189
  query.bodyParts.push(mimeKey);
82
190
  }
83
191
  }
84
- query.bodyParts.push({
192
+ contentRequest = {
85
193
  key: part,
86
194
  start: processed,
87
195
  maxLength: chunkSize
88
- });
196
+ };
197
+ query.bodyParts.push(contentRequest);
89
198
  }
90
- let response = await client.fetchOne(range, query, downloadOptions);
199
+ let expected = {
200
+ uid: uid || requestedUid(range, downloadOptions),
201
+ origins: new Map([[part || '', processed]])
202
+ };
203
+ let response = await fetchExpected(client, range, query, downloadOptions, expected);
91
204
  if (!response) {
92
205
  return { response: false, chunk: false };
93
206
  }
@@ -97,6 +210,12 @@ export async function downloadMessage(client, range, part, options) {
97
210
  range = uid;
98
211
  downloadOptions.uid = true;
99
212
  }
213
+ if (mimeKey && contentRequest) {
214
+ let dropped = droppedSection(mimeKey === 'header' ? response.headers : response.bodyParts?.get(mimeKey), response.bodyParts?.get(part), mimeKey, contentRequest);
215
+ if (dropped) {
216
+ await refetchDroppedSections(client, response, range, downloadOptions, [dropped]);
217
+ }
218
+ }
100
219
  let chunk = !part ? response.source : response.bodyParts && response.bodyParts.get(part);
101
220
  if (!chunk) {
102
221
  return {};
@@ -451,16 +570,31 @@ export async function downloadMessageParts(client, range, parts, options) {
451
570
  // again on the answer for servers that ignore the partial specifier
452
571
  let maxBytes = normalizeByteLimit(downloadOptions.maxBytes);
453
572
  let query = { bodyParts: [] };
573
+ let contentRequests = new Map();
454
574
  for (let part of parts) {
455
575
  query.bodyParts.push(part + '.mime');
456
576
  // The partial specifier carries a 32-bit length (RFC 9051 "number"), so a cap beyond
457
577
  // that is applied on the answer alone
458
- query.bodyParts.push(maxBytes > 0xffffffff ? part : { key: part, start: 0, maxLength: maxBytes });
578
+ let contentRequest = maxBytes > 0xffffffff ? part : { key: part, start: 0, maxLength: maxBytes };
579
+ contentRequests.set(part, contentRequest);
580
+ query.bodyParts.push(contentRequest);
459
581
  }
460
- let response = await client.fetchOne(range, query, downloadOptions);
582
+ let response = await fetchExpected(client, range, query, downloadOptions, {
583
+ uid: requestedUid(range, downloadOptions),
584
+ origins: partialStarts(query.bodyParts)
585
+ });
461
586
  if (!response || !response.bodyParts) {
462
587
  return {};
463
588
  }
589
+ let dropped = [];
590
+ for (let [part, contentRequest] of contentRequests) {
591
+ let section = droppedSection(response.bodyParts.get(part + '.mime'), response.bodyParts.get(part), part + '.mime', contentRequest);
592
+ if (section) {
593
+ dropped.push(section);
594
+ }
595
+ }
596
+ // Sections of different parts do not collide, so every dropped one fits in one FETCH
597
+ await refetchDroppedSections(client, response, range, downloadOptions, dropped);
464
598
  let data = {};
465
599
  for (let [part, content] of response.bodyParts) {
466
600
  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
  /**
@@ -1459,9 +1459,8 @@ export class ImapFlow extends EventEmitter {
1459
1459
  // of the IP - accepting any "localhost" certificate for any IP-hosted
1460
1460
  // server, and rejecting legitimate IP-SAN certificates.
1461
1461
  host: this.host,
1462
- servername: this.servername,
1463
1462
  port: this.port
1464
- }, this.options.tls || {});
1463
+ }, this.tlsServername(), this.options.tls || {});
1465
1464
  this.clearSocketHandlers();
1466
1465
  let settled = false;
1467
1466
  // Single settlement path for the upgrade. Every terminal outcome - handshake
@@ -1851,12 +1850,18 @@ export class ImapFlow extends EventEmitter {
1851
1850
  uids = untagged.attributes[0].value;
1852
1851
  }
1853
1852
  let uidList = expandRange(uids);
1853
+ let earlier = tags.includes('EARLIER');
1854
+ // RFC 7162 section 3.2.10: unlike VANISHED (EARLIER), a plain VANISHED reports messages the
1855
+ // client knows about and decrements the message count like the same number of EXPUNGEs would
1856
+ if (!earlier) {
1857
+ mailbox.exists = Math.max(0, mailbox.exists - uidList.length);
1858
+ }
1854
1859
  for (let uid of uidList) {
1855
1860
  let payload = {
1856
1861
  path: mailbox.path,
1857
1862
  uid,
1858
1863
  vanished: true,
1859
- earlier: tags.includes('EARLIER')
1864
+ earlier
1860
1865
  };
1861
1866
  await this.notifyExpunge(payload);
1862
1867
  }
@@ -1868,7 +1873,7 @@ export class ImapFlow extends EventEmitter {
1868
1873
  // mailbox closed, ignore
1869
1874
  return;
1870
1875
  }
1871
- let message = await formatMessageResponse(untagged, mailbox, this.idHashAlgorithm);
1876
+ let message = await formatMessageResponse(untagged, mailbox, this.idHashAlgorithm, this);
1872
1877
  if (message.flags) {
1873
1878
  let updateEvent = {
1874
1879
  path: mailbox.path,
@@ -1897,6 +1902,12 @@ export class ImapFlow extends EventEmitter {
1897
1902
  }
1898
1903
  return true;
1899
1904
  }
1905
+ // servername for tls.connect(), left out for an IP literal host (this.servername is false then):
1906
+ // Node treats a false value like a missing one, but Bun throws a TypeError for it
1907
+ /** @internal */
1908
+ tlsServername() {
1909
+ return this.servername ? { servername: this.servername } : {};
1910
+ }
1900
1911
  // Normalizes a message range from various input formats into an IMAP-compatible
1901
1912
  // sequence string (e.g., "1:5,7,10:*"). Handles: numbers, "*", {all:true},
1902
1913
  // {uid:value}, search query objects (resolved via SEARCH), and arrays of numbers.
@@ -1939,6 +1950,11 @@ export class ImapFlow extends EventEmitter {
1939
1950
  if (!value) {
1940
1951
  return false;
1941
1952
  }
1953
+ // An empty mailbox has no message numbers: every sequence set, "1:*" included, would get a
1954
+ // BAD (RFC 9051 section 9, seq-number). UID sets may point past the end, so they are sent.
1955
+ if (!options.uid && this.mailbox && !this.mailbox.exists) {
1956
+ return false;
1957
+ }
1942
1958
  return value;
1943
1959
  }
1944
1960
  // The single definition of "the connection is not free". A held or queued mailbox lock, a
@@ -2008,9 +2024,8 @@ export class ImapFlow extends EventEmitter {
2008
2024
  let connector = this.secureConnection ? tls : net;
2009
2025
  let opts = Object.assign({
2010
2026
  host: this.host,
2011
- servername: this.servername,
2012
2027
  port: this.port
2013
- }, this.options.tls || {});
2028
+ }, this.tlsServername(), this.options.tls || {});
2014
2029
  this.untaggedHandlers.OK = (...args) => this.initialOK(...args);
2015
2030
  this.untaggedHandlers.BYE = (...args) => this.serverBye(...args);
2016
2031
  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/";
@@ -1,4 +1,4 @@
1
1
  // Generated by scripts/build.js from package.json. Do not edit by hand.
2
2
  export const name = "imapflow";
3
- export const version = "2.2.7";
3
+ export const version = "2.2.8";
4
4
  export const homepage = "https://imapflow.com/";
@@ -368,10 +368,13 @@ export const searchCompiler = (connection, query) => {
368
368
  fail('InvalidSearchQuery', `Search value for ${term.toLowerCase()} must be a string`);
369
369
  }
370
370
  let flag = formatFlag(params[term]);
371
- // formatFlag() refuses \Recent, which is not a keyword. Dropping the
372
- // criterion would widen the search, so the query is refused instead
371
+ // formatFlag() refuses \Recent, which is not a keyword, and values that are
372
+ // not atoms. Dropping the criterion would widen the search, so the query is
373
+ // refused instead
373
374
  if (flag === false) {
374
- fail('InvalidSearchQuery', `${params[term]} can not be searched as a keyword, use the "recent" search key instead`);
375
+ fail('InvalidSearchQuery', /^\\recent$/i.test(params[term])
376
+ ? `${params[term]} can not be searched as a keyword, use the "recent" search key instead`
377
+ : `${params[term]} is not a valid keyword`);
375
378
  }
376
379
  // Compiled even when the mailbox does not allow the keyword: the
377
380
  // 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
@@ -2,6 +2,7 @@
2
2
  import libmime from 'libmime';
3
3
  import { resolveCharset } from './charsets.js';
4
4
  import { compiler } from './handler/imap-handler.js';
5
+ import imapFormalSyntax from './handler/imap-formal-syntax.js';
5
6
  import { createHash } from 'node:crypto';
6
7
  import { JPDecoder } from './jp-decoder.js';
7
8
  import iconv from 'iconv-lite';
@@ -280,7 +281,8 @@ export function buildStatusQueryAttributes(connection, statusQuery) {
280
281
  }
281
282
  break;
282
283
  case 'HIGHESTMODSEQ':
283
- if (connection.capabilities.has('CONDSTORE')) {
284
+ // QRESYNC implies CONDSTORE (RFC 7162 3.2.3)
285
+ if (connection.capabilities.has('CONDSTORE') || connection.capabilities.has('QRESYNC')) {
284
286
  attributes.push({ type: 'ATOM', value: key.toUpperCase() });
285
287
  }
286
288
  break;
@@ -661,24 +663,40 @@ export function getColorFlags(color) {
661
663
  * @param untagged - Parsed untagged IMAP response
662
664
  * @param mailbox - Current mailbox state object
663
665
  * @param idHashAlgorithm - Hash for the fallback message id, `md5` unless the client was told otherwise
666
+ * @param connection - Connection the response arrived on, decodes Gmail labels like mailbox names
664
667
  * @returns Formatted message object with properties like seq, uid, flags, envelope, etc.
665
668
  */
666
- export async function formatMessageResponse(untagged, mailbox, idHashAlgorithm) {
669
+ export async function formatMessageResponse(untagged, mailbox, idHashAlgorithm, connection) {
667
670
  let map = {};
668
671
  // The sequence number indexes into mailbox state, so an unusable one is dropped rather
669
672
  // than coerced to NaN or Infinity
670
673
  map.seq = parseUintValue(untagged.command, MAX_UINT32_DIGITS) || undefined;
671
674
  let key;
675
+ // the <origin> of a partial section ("BODY[2]<1024>"), kept for the value that follows
676
+ let origin = false;
677
+ let partialOrigins;
678
+ let recordOrigin = (sectionKey) => {
679
+ if (origin !== false) {
680
+ if (!partialOrigins) {
681
+ partialOrigins = new Map();
682
+ }
683
+ partialOrigins.set(sectionKey, origin);
684
+ }
685
+ };
672
686
  let attributes = ((untagged.attributes && untagged.attributes[1]) || []);
673
687
  for (let i = 0, len = attributes.length; i < len; i++) {
674
688
  let attribute = attributes[i];
675
689
  if (i % 2 === 0) {
690
+ origin = false;
676
691
  key = (await compiler({
677
692
  attributes: [attribute]
678
693
  }))
679
694
  .toString()
680
695
  .toLowerCase()
681
- .replace(/<\d+(\.\d+)?>$/, '');
696
+ .replace(/<(\d+)(\.\d+)?>$/, (match, start) => {
697
+ origin = parseUintValue(start, MAX_UINT32_DIGITS);
698
+ return '';
699
+ });
682
700
  continue;
683
701
  }
684
702
  /* c8 ignore start */ // defensive: key is always a string produced by the compiler above
@@ -723,6 +741,7 @@ export async function formatMessageResponse(untagged, mailbox, idHashAlgorithm)
723
741
  case 'body[]':
724
742
  case 'binary[]':
725
743
  map.source = getBuffer(attribute);
744
+ recordOrigin('');
726
745
  break;
727
746
  case 'uid':
728
747
  // A UID feeds mailbox.uidNext one line below, and from there every range
@@ -769,7 +788,8 @@ export async function formatMessageResponse(untagged, mailbox, idHashAlgorithm)
769
788
  map.threadId = getString(attribute);
770
789
  break;
771
790
  case 'x-gm-labels':
772
- map.labels = new Set(getArray(attribute));
791
+ // labels are mailbox names, modified UTF-7 unless UTF-8 is enabled
792
+ map.labels = new Set(getArray(attribute).map(label => (connection ? decodePath(connection, label) : label)));
773
793
  break;
774
794
  case 'rfc822.size':
775
795
  map.size = getUint(attribute) || 0;
@@ -815,6 +835,7 @@ export async function formatMessageResponse(untagged, mailbox, idHashAlgorithm)
815
835
  map.bodyParts = new Map();
816
836
  }
817
837
  map.bodyParts.set(partKey, value);
838
+ recordOrigin(partKey);
818
839
  if (match[1].toLowerCase() === 'binary') {
819
840
  // The part arrived via FETCH BINARY (RFC 3516, FETCH side folded
820
841
  // into IMAP4rev2), so the server has already removed the
@@ -856,6 +877,10 @@ export async function formatMessageResponse(untagged, mailbox, idHashAlgorithm)
856
877
  .update([path, mailbox.uidValidity?.toString() || '', map.uid.toString()].join(':'))
857
878
  .digest('hex');
858
879
  }
880
+ if (partialOrigins) {
881
+ // non-enumerable, so it stays out of logged and serialized fetch results
882
+ Object.defineProperty(map, 'partialOrigins', { value: partialOrigins, writable: true, configurable: true });
883
+ }
859
884
  if (map.flags) {
860
885
  let flagColor = getFlagColor(map.flags);
861
886
  if (flagColor) {
@@ -1298,14 +1323,22 @@ export function formatDateTime(value) {
1298
1323
  let timeStr = date.toISOString().substring(11, 19);
1299
1324
  return `${dateStr} ${timeStr} +0000`;
1300
1325
  }
1326
+ // the memoized ATOM-CHAR set of RFC 9051 section 9
1327
+ const atomChars = imapFormalSyntax['ATOM-CHAR'];
1301
1328
  /**
1302
- * Normalizes a flag string. Returns false for non-settable flags (e.g. \Recent),
1303
- * and capitalizes system flags properly.
1329
+ * Normalizes a flag string. Returns false for non-settable flags (e.g. \Recent) and for
1330
+ * values that are not valid flags (keywords must be atoms), and capitalizes system flags properly.
1304
1331
  *
1305
1332
  * @param flag - Flag string to normalize
1306
1333
  * @returns Normalized flag string, or false if the flag cannot be set
1307
1334
  */
1308
1335
  export function formatFlag(flag) {
1336
+ // RFC 9051 section 9: flag-keyword is an atom and flag-extension is "\\" atom, the same check
1337
+ // the compiler uses to decide what it can send unquoted
1338
+ let atom = flag.charAt(0) === '\\' ? flag.slice(1) : flag;
1339
+ if (!atom || imapFormalSyntax.verify(atom, atomChars()) >= 0) {
1340
+ return false;
1341
+ }
1309
1342
  switch (flag.toLowerCase()) {
1310
1343
  case '\\recent':
1311
1344
  // can not set or remove
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imapflow",
3
- "version": "2.2.7",
3
+ "version": "2.2.8",
4
4
  "description": "IMAP Client for Node",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/imap-flow.js",
@@ -51,7 +51,8 @@
51
51
  "lint": "npm run typecheck && eslint .",
52
52
  "lint:fix": "eslint . --fix",
53
53
  "prepare": "npm run build",
54
- "update": "rm -rf node_modules package-lock.json && ncu -u && npm install"
54
+ "update": "rm -rf node_modules package-lock.json && ncu -u && npm install",
55
+ "test:james": "bash test/integration/run-james-tests.sh"
55
56
  },
56
57
  "repository": {
57
58
  "type": "git",
@@ -74,12 +75,13 @@
74
75
  "eslint": "10.12.0",
75
76
  "eslint-config-prettier": "10.1.8",
76
77
  "globals": "17.13.0",
78
+ "imapkit": "4.1.0",
77
79
  "prettier": "3.9.9",
78
80
  "tsx": "4.23.15",
79
81
  "types-node-legacy": "npm:@types/node@20.0.0",
80
82
  "typescript": "6.0.3",
81
83
  "typescript-eslint": "8.71.1",
82
- "wrangler": "4.147.0"
84
+ "wrangler": "4.148.0"
83
85
  },
84
86
  "dependencies": {
85
87
  "@zone-eu/mailsplit": "5.4.20",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@withbuddi/buddi",
3
- "version": "0.1.0-pre.49",
3
+ "version": "0.1.0-pre.51",
4
4
  "type": "module",
5
5
  "description": "Your personal agents, on your computer",
6
6
  "repository": {
@@ -17,7 +17,7 @@
17
17
  "buddi": "packages/install/dist/launcher.js"
18
18
  },
19
19
  "buddi": {
20
- "notes": "### Added\n\n- Host API 1.33 canvas renderers: `story` draws a plugin's story row (or a list of them) in the plugin's own words, `audio` plays a saved recording, and `query` draws a plugin's own page components against one of its page queries.\n\n- Host API 1.33 `messenger.mediaFirst` on a view and a reserved `attachments` field on a tool's output: Telegram sends the plugin's own cached picture or saved recording first, then the full answer below it. Attachments are checked against the tool's own plugin.\n\n- Host API 1.33 `approvals.configuredForRun` and the `consent_for_run` export: the plugin a mission run was started from may vouch for a tool in that run, on the owner's own settings, rechecked on every call.\n\n- Host API 1.33 `sheet` (a drawer opened by a page parameter) and `digest` (a saved digest card, with its recording) page components, and links that set page parameters.\n\n- Story cards, drawers and canvas show cached publisher feed images with source credits; plugin assets support an aspect-preserving 768px variant.\n\n### Fixed\n\n- A Telegram answer that Telegram refuses to format is sent again as plain text instead of being lost.\n\n- Every Telegram reply now renders light Markdown as safe Telegram HTML: clickable source names, bold labels and lists.\n\n- Saved recordings a tool's result lists also play in the dashboard transcript under the tool's row.\n\n- Audio waveforms fill the full seek area in wide canvas cards, keeping visual progress aligned with playback position.\n\n### Changed\n\n- Collapse appended mission material in chat while keeping its original text available for inspection.\n\n- Recording players support seeking and an MP3 download made in the browser; canvas results keep their technical details collapsed.\n\n- Chat keeps its original text size by default; A−, A+ and Reset in the chat menu adjust message text and tables, remembered in this browser.\n\n- Restore paragraph spacing in rendered chat replies so separate paragraphs remain easy to scan.\n\n- Inspect unrendered response text blocks from each chat reply; story drawer actions stay visible while long content scrolls.\n\n- A story's Ask button carries a removable story reference into a new conversation, with question buttons that send immediately when clicked and preserve your composer draft. Opening chat sends nothing.\n\n- Dashboard restart supports systemd and launchd; configured supervisor sockets are checked before a restart is offered.\n\n- Story rows may label where their headline, summary and update came from (the plugin's own words), and an update that repeats the summary is not drawn twice.\n\n- Host API 1.33 adds compact setup cards to plugin pages.\n\n- Plugin settings sections inside a polling container retain separate panels and header actions.\n\n- Plugin installs show preparation progress immediately, keep long descriptions in an expandable review with visible install actions, and guide you through restarting and setup. Managed development installs can restart from the dashboard; checking an already-current plugin says it is up to date."
20
+ "notes": "### Added\n\n- Run now on a mission: on its row in Missions, `buddi missions run <id>` or `POST /api/missions/:id/run`. It runs at once exactly as the schedule would, report and notifications included, says \"Running…\" until done, and counts like any run.\n\n### Changed\n\n- buddi.app's Node is pinned to one exact version (22.23.3) instead of the newest 22.x at build time, and a release is not tagged when buddi would need a newer Node than the app ships or the app would ship without npm.\n\n### Fixed\n\n- Approving a schedule an agent proposed without an end date works again; it was refused every time with \"the effect changed since its preview\". Such a watch now ends with the 30th day in the owner's zone, and every approval card is described at the same instant it is recorded."
21
21
  },
22
22
  "files": [
23
23
  "packages",