imapflow 2.2.1 → 2.2.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.2.2](https://github.com/postalsys/imapflow/compare/v2.2.1...v2.2.2) (2026-10-03)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * do not let a flag update that reduces to nothing clear every flag ([686a04b](https://github.com/postalsys/imapflow/commit/686a04b6c29d5f97afdbb03779e7b29d102cdef3))
9
+ * keep append flags when the destination is not an open read-write mailbox ([d7f8da3](https://github.com/postalsys/imapflow/commit/d7f8da3131d8df5b5607021dbbca27445bb973c2)), closes [#415](https://github.com/postalsys/imapflow/issues/415)
10
+
3
11
  ## [2.2.1](https://github.com/postalsys/imapflow/compare/v2.2.0...v2.2.1) (2026-10-01)
4
12
 
5
13
 
@@ -37,10 +37,34 @@ async function append(connection, destination, content, flags, idate) {
37
37
  let selected = (0, tools_js_1.getSelectedMailbox)(connection);
38
38
  // The selected mailbox when appending to it, false otherwise
39
39
  const targetMailbox = selected && (0, tools_js_1.comparePaths)(connection, selected.path, destination) ? selected : false;
40
- // Validate and format flags. Only flags allowed by the mailbox's permanentFlags are included.
40
+ // Flags are only filtered when a read-write selection of the destination itself says what it
41
+ // accepts: the permanentFlags of an unrelated mailbox say nothing about the destination, and a
42
+ // read-only selection reports an empty PERMANENTFLAGS list (Dovecot does), which canUseFlag()
43
+ // reads as deny-all. Otherwise they go to the server unfiltered and it decides (RFC 9051).
44
+ const flagSource = targetMailbox && !targetMailbox.readOnly ? targetMailbox : false;
45
+ const dropped = [];
41
46
  flags = (Array.isArray(flags) ? flags : [].concat(flags || []))
42
- .map(flag => flag && (0, tools_js_1.formatFlag)(flag.toString()))
43
- .filter((flag) => !!flag && (0, tools_js_1.canUseFlag)(connection.mailbox, flag));
47
+ .map(flag => {
48
+ const formatted = flag && (0, tools_js_1.formatFlag)(flag.toString());
49
+ if (!formatted || !(0, tools_js_1.canUseFlag)(flagSource, formatted)) {
50
+ if (flag) {
51
+ dropped.push(flag.toString());
52
+ }
53
+ return false;
54
+ }
55
+ return formatted;
56
+ })
57
+ .filter((flag) => !!flag);
58
+ // The stored message simply ends up without them, and the caller is told nothing, so leave
59
+ // a trail for the flags that never reached the server.
60
+ if (dropped.length) {
61
+ connection.log.warn({
62
+ msg: 'Dropped flags the mailbox does not accept',
63
+ cid: connection.id,
64
+ path: destination,
65
+ dropped
66
+ });
67
+ }
44
68
  // APPEND command format: APPEND <mailbox> [<flags>] [<date-time>] <literal>
45
69
  let attributes = [{ type: 'ATOM', value: (0, tools_js_1.encodePath)(connection, destination) }];
46
70
  // Internal date: the date the server should record for this message.
@@ -31,8 +31,11 @@ async function store(connection, range, flags, options) {
31
31
  else if (options.silent) {
32
32
  operation = `${operation}.SILENT`;
33
33
  }
34
+ // Normalized once: the raw value is also compared below, and a mixed-case 'SET' must not
35
+ // take the set branch here and the add branch there.
36
+ const operationName = (options.operation || '').toLowerCase();
34
37
  // Prefix determines the operation: none = set (replace), + = add, - = remove
35
- switch ((options.operation || '').toLowerCase()) {
38
+ switch (operationName) {
36
39
  case 'set':
37
40
  break;
38
41
  case 'remove':
@@ -43,20 +46,42 @@ async function store(connection, range, flags, options) {
43
46
  operation = `+${operation}`;
44
47
  break;
45
48
  }
46
- // Validate each flag: format it (normalize backslash prefix for system flags),
47
- // then check if the mailbox's permanentFlags allow it. Removal is always allowed
49
+ // Only an explicitly empty array asks for every flag to be cleared. A missing value is not
50
+ // that request, and neither is a 'set' whose flags all get dropped below: either would
51
+ // compile to "FLAGS ()" and wipe the message instead of storing what was asked for. The
52
+ // line is drawn at destroying flags, not at fidelity, so a set that keeps some of the
53
+ // requested flags still runs, as the documented contract for those methods says.
54
+ const clearAll = operationName === 'set' && Array.isArray(flags) && !flags.length;
55
+ // permanentFlags lists the IMAP keywords the mailbox accepts and says nothing about Gmail
56
+ // labels, so it is not consulted for X-GM-LABELS - the same reason an unrelated mailbox's
57
+ // flags are not consulted for APPEND (issue #415).
58
+ const flagSource = options.useLabels ? false : mailbox;
59
+ // Validate each flag: format it (normalize backslash prefix for system flags, reject the
60
+ // server-owned \Recent), then check that the mailbox allows it. Removal is always allowed
48
61
  // since it doesn't require the flag to be in permanentFlags.
62
+ const dropped = [];
49
63
  flags = (Array.isArray(flags) ? flags : [].concat(flags || []))
50
64
  .map(flag => {
51
65
  let formatted = (0, tools_js_1.formatFlag)(flag);
52
- if (!(0, tools_js_1.canUseFlag)(mailbox, formatted) && options.operation !== 'remove') {
66
+ if (!formatted || (!(0, tools_js_1.canUseFlag)(flagSource, formatted) && operationName !== 'remove')) {
67
+ dropped.push(flag);
53
68
  return false;
54
69
  }
55
70
  return formatted;
56
71
  })
57
72
  .filter((flag) => !!flag);
58
- // Allow empty flags only for 'set' operation (which clears all flags)
59
- if (!flags.length && options.operation !== 'set') {
73
+ // The caller is told nothing by the boolean this returns, so leave a trail for the flags
74
+ // that never reached the server.
75
+ if (dropped.length) {
76
+ connection.log.warn({
77
+ msg: 'Dropped flags the mailbox does not accept',
78
+ cid: connection.id,
79
+ path: mailbox.path,
80
+ operation,
81
+ dropped
82
+ });
83
+ }
84
+ if (!flags.length && !clearAll) {
60
85
  return false;
61
86
  }
62
87
  let attributes = [
@@ -356,7 +356,7 @@ export declare class ImapFlow extends EventEmitter {
356
356
  * Sets flags for a message or message range
357
357
  *
358
358
  * @param range Range to filter the messages
359
- * @param flags Array of flags to set. Only flags that are permitted to set are used, other flags are ignored
359
+ * @param flags Array of flags to set. Only flags that are permitted to set are used, other flags are ignored. An empty array clears every flag, but a non-empty array with nothing usable in it is refused rather than clearing the message
360
360
  * @param options Store options
361
361
  * @returns Did the operation succeed or not
362
362
  *
@@ -2631,7 +2631,7 @@ class ImapFlow extends node_events_1.EventEmitter {
2631
2631
  * Sets flags for a message or message range
2632
2632
  *
2633
2633
  * @param range Range to filter the messages
2634
- * @param flags Array of flags to set. Only flags that are permitted to set are used, other flags are ignored
2634
+ * @param flags Array of flags to set. Only flags that are permitted to set are used, other flags are ignored. An empty array clears every flag, but a non-empty array with nothing usable in it is refused rather than clearing the message
2635
2635
  * @param options Store options
2636
2636
  * @returns Did the operation succeed or not
2637
2637
  *
@@ -1,3 +1,3 @@
1
1
  export declare const name = "imapflow";
2
- export declare const version = "2.2.1";
2
+ export declare const version = "2.2.2";
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.1";
6
+ exports.version = "2.2.2";
7
7
  exports.homepage = "https://imapflow.com/";
@@ -34,10 +34,34 @@ export default async function append(connection, destination, content, flags, id
34
34
  let selected = getSelectedMailbox(connection);
35
35
  // The selected mailbox when appending to it, false otherwise
36
36
  const targetMailbox = selected && comparePaths(connection, selected.path, destination) ? selected : false;
37
- // Validate and format flags. Only flags allowed by the mailbox's permanentFlags are included.
37
+ // Flags are only filtered when a read-write selection of the destination itself says what it
38
+ // accepts: the permanentFlags of an unrelated mailbox say nothing about the destination, and a
39
+ // read-only selection reports an empty PERMANENTFLAGS list (Dovecot does), which canUseFlag()
40
+ // reads as deny-all. Otherwise they go to the server unfiltered and it decides (RFC 9051).
41
+ const flagSource = targetMailbox && !targetMailbox.readOnly ? targetMailbox : false;
42
+ const dropped = [];
38
43
  flags = (Array.isArray(flags) ? flags : [].concat(flags || []))
39
- .map(flag => flag && formatFlag(flag.toString()))
40
- .filter((flag) => !!flag && canUseFlag(connection.mailbox, flag));
44
+ .map(flag => {
45
+ const formatted = flag && formatFlag(flag.toString());
46
+ if (!formatted || !canUseFlag(flagSource, formatted)) {
47
+ if (flag) {
48
+ dropped.push(flag.toString());
49
+ }
50
+ return false;
51
+ }
52
+ return formatted;
53
+ })
54
+ .filter((flag) => !!flag);
55
+ // The stored message simply ends up without them, and the caller is told nothing, so leave
56
+ // a trail for the flags that never reached the server.
57
+ if (dropped.length) {
58
+ connection.log.warn({
59
+ msg: 'Dropped flags the mailbox does not accept',
60
+ cid: connection.id,
61
+ path: destination,
62
+ dropped
63
+ });
64
+ }
41
65
  // APPEND command format: APPEND <mailbox> [<flags>] [<date-time>] <literal>
42
66
  let attributes = [{ type: 'ATOM', value: encodePath(connection, destination) }];
43
67
  // Internal date: the date the server should record for this message.
@@ -28,8 +28,11 @@ export default async function store(connection, range, flags, options) {
28
28
  else if (options.silent) {
29
29
  operation = `${operation}.SILENT`;
30
30
  }
31
+ // Normalized once: the raw value is also compared below, and a mixed-case 'SET' must not
32
+ // take the set branch here and the add branch there.
33
+ const operationName = (options.operation || '').toLowerCase();
31
34
  // Prefix determines the operation: none = set (replace), + = add, - = remove
32
- switch ((options.operation || '').toLowerCase()) {
35
+ switch (operationName) {
33
36
  case 'set':
34
37
  break;
35
38
  case 'remove':
@@ -40,20 +43,42 @@ export default async function store(connection, range, flags, options) {
40
43
  operation = `+${operation}`;
41
44
  break;
42
45
  }
43
- // Validate each flag: format it (normalize backslash prefix for system flags),
44
- // then check if the mailbox's permanentFlags allow it. Removal is always allowed
46
+ // Only an explicitly empty array asks for every flag to be cleared. A missing value is not
47
+ // that request, and neither is a 'set' whose flags all get dropped below: either would
48
+ // compile to "FLAGS ()" and wipe the message instead of storing what was asked for. The
49
+ // line is drawn at destroying flags, not at fidelity, so a set that keeps some of the
50
+ // requested flags still runs, as the documented contract for those methods says.
51
+ const clearAll = operationName === 'set' && Array.isArray(flags) && !flags.length;
52
+ // permanentFlags lists the IMAP keywords the mailbox accepts and says nothing about Gmail
53
+ // labels, so it is not consulted for X-GM-LABELS - the same reason an unrelated mailbox's
54
+ // flags are not consulted for APPEND (issue #415).
55
+ const flagSource = options.useLabels ? false : mailbox;
56
+ // Validate each flag: format it (normalize backslash prefix for system flags, reject the
57
+ // server-owned \Recent), then check that the mailbox allows it. Removal is always allowed
45
58
  // since it doesn't require the flag to be in permanentFlags.
59
+ const dropped = [];
46
60
  flags = (Array.isArray(flags) ? flags : [].concat(flags || []))
47
61
  .map(flag => {
48
62
  let formatted = formatFlag(flag);
49
- if (!canUseFlag(mailbox, formatted) && options.operation !== 'remove') {
63
+ if (!formatted || (!canUseFlag(flagSource, formatted) && operationName !== 'remove')) {
64
+ dropped.push(flag);
50
65
  return false;
51
66
  }
52
67
  return formatted;
53
68
  })
54
69
  .filter((flag) => !!flag);
55
- // Allow empty flags only for 'set' operation (which clears all flags)
56
- if (!flags.length && options.operation !== 'set') {
70
+ // The caller is told nothing by the boolean this returns, so leave a trail for the flags
71
+ // that never reached the server.
72
+ if (dropped.length) {
73
+ connection.log.warn({
74
+ msg: 'Dropped flags the mailbox does not accept',
75
+ cid: connection.id,
76
+ path: mailbox.path,
77
+ operation,
78
+ dropped
79
+ });
80
+ }
81
+ if (!flags.length && !clearAll) {
57
82
  return false;
58
83
  }
59
84
  let attributes = [
@@ -356,7 +356,7 @@ export declare class ImapFlow extends EventEmitter {
356
356
  * Sets flags for a message or message range
357
357
  *
358
358
  * @param range Range to filter the messages
359
- * @param flags Array of flags to set. Only flags that are permitted to set are used, other flags are ignored
359
+ * @param flags Array of flags to set. Only flags that are permitted to set are used, other flags are ignored. An empty array clears every flag, but a non-empty array with nothing usable in it is refused rather than clearing the message
360
360
  * @param options Store options
361
361
  * @returns Did the operation succeed or not
362
362
  *
@@ -2590,7 +2590,7 @@ export class ImapFlow extends EventEmitter {
2590
2590
  * Sets flags for a message or message range
2591
2591
  *
2592
2592
  * @param range Range to filter the messages
2593
- * @param flags Array of flags to set. Only flags that are permitted to set are used, other flags are ignored
2593
+ * @param flags Array of flags to set. Only flags that are permitted to set are used, other flags are ignored. An empty array clears every flag, but a non-empty array with nothing usable in it is refused rather than clearing the message
2594
2594
  * @param options Store options
2595
2595
  * @returns Did the operation succeed or not
2596
2596
  *
@@ -1,3 +1,3 @@
1
1
  export declare const name = "imapflow";
2
- export declare const version = "2.2.1";
2
+ export declare const version = "2.2.2";
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.1";
3
+ export const version = "2.2.2";
4
4
  export const homepage = "https://imapflow.com/";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imapflow",
3
- "version": "2.2.1",
3
+ "version": "2.2.2",
4
4
  "description": "IMAP Client for Node",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/imap-flow.js",
@@ -70,7 +70,7 @@
70
70
  "devDependencies": {
71
71
  "@types/node": "20.19.43",
72
72
  "c8": "12.0.0",
73
- "eslint": "10.11.0",
73
+ "eslint": "10.12.0",
74
74
  "eslint-config-prettier": "10.1.8",
75
75
  "globals": "17.13.0",
76
76
  "prettier": "3.9.9",
@@ -78,7 +78,7 @@
78
78
  "types-node-legacy": "npm:@types/node@20.0.0",
79
79
  "typescript": "6.0.3",
80
80
  "typescript-eslint": "8.71.0",
81
- "wrangler": "4.145.0"
81
+ "wrangler": "4.147.0"
82
82
  },
83
83
  "dependencies": {
84
84
  "@zone-eu/mailsplit": "5.4.19",
@@ -87,7 +87,7 @@
87
87
  "libbase64": "1.3.1",
88
88
  "libmime": "5.4.6",
89
89
  "libqp": "2.1.2",
90
- "pino": "10.3.1",
90
+ "pino": "10.4.0",
91
91
  "socks": "2.8.10"
92
92
  },
93
93
  "engines": {