@rndmjoker/proton-mcp 0.4.4

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 (141) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +405 -0
  3. package/dist/activity.d.ts +49 -0
  4. package/dist/activity.js +72 -0
  5. package/dist/activity.js.map +1 -0
  6. package/dist/bridge/certificate.d.ts +75 -0
  7. package/dist/bridge/certificate.js +176 -0
  8. package/dist/bridge/certificate.js.map +1 -0
  9. package/dist/bridge/connection.d.ts +99 -0
  10. package/dist/bridge/connection.js +244 -0
  11. package/dist/bridge/connection.js.map +1 -0
  12. package/dist/bridge/errors.d.ts +35 -0
  13. package/dist/bridge/errors.js +56 -0
  14. package/dist/bridge/errors.js.map +1 -0
  15. package/dist/config.d.ts +92 -0
  16. package/dist/config.js +142 -0
  17. package/dist/config.js.map +1 -0
  18. package/dist/credentials/availability.d.ts +62 -0
  19. package/dist/credentials/availability.js +107 -0
  20. package/dist/credentials/availability.js.map +1 -0
  21. package/dist/credentials/encrypted-file.d.ts +50 -0
  22. package/dist/credentials/encrypted-file.js +156 -0
  23. package/dist/credentials/encrypted-file.js.map +1 -0
  24. package/dist/credentials/keyring.d.ts +48 -0
  25. package/dist/credentials/keyring.js +97 -0
  26. package/dist/credentials/keyring.js.map +1 -0
  27. package/dist/credentials/plain-file.d.ts +24 -0
  28. package/dist/credentials/plain-file.js +78 -0
  29. package/dist/credentials/plain-file.js.map +1 -0
  30. package/dist/credentials/probe-keyring.d.ts +11 -0
  31. package/dist/credentials/probe-keyring.js +57 -0
  32. package/dist/credentials/probe-keyring.js.map +1 -0
  33. package/dist/credentials/session.d.ts +15 -0
  34. package/dist/credentials/session.js +21 -0
  35. package/dist/credentials/session.js.map +1 -0
  36. package/dist/credentials/store.d.ts +80 -0
  37. package/dist/credentials/store.js +94 -0
  38. package/dist/credentials/store.js.map +1 -0
  39. package/dist/in-flight.d.ts +33 -0
  40. package/dist/in-flight.js +78 -0
  41. package/dist/in-flight.js.map +1 -0
  42. package/dist/mail/actions.d.ts +116 -0
  43. package/dist/mail/actions.js +265 -0
  44. package/dist/mail/actions.js.map +1 -0
  45. package/dist/mail/attachments.d.ts +39 -0
  46. package/dist/mail/attachments.js +86 -0
  47. package/dist/mail/attachments.js.map +1 -0
  48. package/dist/mail/compose.d.ts +264 -0
  49. package/dist/mail/compose.js +395 -0
  50. package/dist/mail/compose.js.map +1 -0
  51. package/dist/mail/drafts.d.ts +138 -0
  52. package/dist/mail/drafts.js +449 -0
  53. package/dist/mail/drafts.js.map +1 -0
  54. package/dist/mail/ids.d.ts +78 -0
  55. package/dist/mail/ids.js +135 -0
  56. package/dist/mail/ids.js.map +1 -0
  57. package/dist/mail/labels.d.ts +61 -0
  58. package/dist/mail/labels.js +139 -0
  59. package/dist/mail/labels.js.map +1 -0
  60. package/dist/mail/markup.d.ts +91 -0
  61. package/dist/mail/markup.js +265 -0
  62. package/dist/mail/markup.js.map +1 -0
  63. package/dist/mail/messages.d.ts +147 -0
  64. package/dist/mail/messages.js +219 -0
  65. package/dist/mail/messages.js.map +1 -0
  66. package/dist/mail/quote.d.ts +65 -0
  67. package/dist/mail/quote.js +157 -0
  68. package/dist/mail/quote.js.map +1 -0
  69. package/dist/mail/search.d.ts +66 -0
  70. package/dist/mail/search.js +107 -0
  71. package/dist/mail/search.js.map +1 -0
  72. package/dist/mail/send.d.ts +61 -0
  73. package/dist/mail/send.js +132 -0
  74. package/dist/mail/send.js.map +1 -0
  75. package/dist/mime/parse.d.ts +110 -0
  76. package/dist/mime/parse.js +194 -0
  77. package/dist/mime/parse.js.map +1 -0
  78. package/dist/server.d.ts +13 -0
  79. package/dist/server.js +387 -0
  80. package/dist/server.js.map +1 -0
  81. package/dist/session.d.ts +86 -0
  82. package/dist/session.js +220 -0
  83. package/dist/session.js.map +1 -0
  84. package/dist/settings.d.ts +52 -0
  85. package/dist/settings.js +80 -0
  86. package/dist/settings.js.map +1 -0
  87. package/dist/tools/actions.d.ts +21 -0
  88. package/dist/tools/actions.js +133 -0
  89. package/dist/tools/actions.js.map +1 -0
  90. package/dist/tools/capabilities.d.ts +52 -0
  91. package/dist/tools/capabilities.js +96 -0
  92. package/dist/tools/capabilities.js.map +1 -0
  93. package/dist/tools/confirm.d.ts +84 -0
  94. package/dist/tools/confirm.js +212 -0
  95. package/dist/tools/confirm.js.map +1 -0
  96. package/dist/tools/drafts.d.ts +21 -0
  97. package/dist/tools/drafts.js +199 -0
  98. package/dist/tools/drafts.js.map +1 -0
  99. package/dist/tools/failures.d.ts +46 -0
  100. package/dist/tools/failures.js +157 -0
  101. package/dist/tools/failures.js.map +1 -0
  102. package/dist/tools/format.d.ts +55 -0
  103. package/dist/tools/format.js +220 -0
  104. package/dist/tools/format.js.map +1 -0
  105. package/dist/tools/guidance.d.ts +38 -0
  106. package/dist/tools/guidance.js +226 -0
  107. package/dist/tools/guidance.js.map +1 -0
  108. package/dist/tools/interface.d.ts +28 -0
  109. package/dist/tools/interface.js +63 -0
  110. package/dist/tools/interface.js.map +1 -0
  111. package/dist/tools/labels.d.ts +21 -0
  112. package/dist/tools/labels.js +92 -0
  113. package/dist/tools/labels.js.map +1 -0
  114. package/dist/tools/list-folders.d.ts +14 -0
  115. package/dist/tools/list-folders.js +70 -0
  116. package/dist/tools/list-folders.js.map +1 -0
  117. package/dist/tools/messages.d.ts +9 -0
  118. package/dist/tools/messages.js +221 -0
  119. package/dist/tools/messages.js.map +1 -0
  120. package/dist/tools/send.d.ts +29 -0
  121. package/dist/tools/send.js +236 -0
  122. package/dist/tools/send.js.map +1 -0
  123. package/dist/tools/sign-in-required.d.ts +94 -0
  124. package/dist/tools/sign-in-required.js +168 -0
  125. package/dist/tools/sign-in-required.js.map +1 -0
  126. package/dist/web/layout.d.ts +75 -0
  127. package/dist/web/layout.js +590 -0
  128. package/dist/web/layout.js.map +1 -0
  129. package/dist/web/pages.d.ts +79 -0
  130. package/dist/web/pages.js +284 -0
  131. package/dist/web/pages.js.map +1 -0
  132. package/dist/web/sections.d.ts +105 -0
  133. package/dist/web/sections.js +207 -0
  134. package/dist/web/sections.js.map +1 -0
  135. package/dist/web/security.d.ts +122 -0
  136. package/dist/web/security.js +180 -0
  137. package/dist/web/security.js.map +1 -0
  138. package/dist/web/server.d.ts +153 -0
  139. package/dist/web/server.js +622 -0
  140. package/dist/web/server.js.map +1 -0
  141. package/package.json +70 -0
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Bookkeeping for tool calls that are still running.
3
+ *
4
+ * Background: a stdio server learns that its client is gone by its input
5
+ * stream ending. If it exits right away it cuts off calls that are still in
6
+ * progress and their answer is lost. If it waits forever it hangs whenever a
7
+ * call never returns.
8
+ *
9
+ * Hence: keep count, and wait for a bounded grace period on shutdown.
10
+ */
11
+ import * as activity from './activity.js';
12
+ let inFlight = 0;
13
+ let idleResolvers = [];
14
+ /** How many calls are currently running. For tests and diagnostics only. */
15
+ export function inFlightCount() {
16
+ return inFlight;
17
+ }
18
+ /**
19
+ * Wraps a tool call so that shutdown waits for it.
20
+ *
21
+ * The counter is also released on failure. Otherwise a single failed call
22
+ * would keep the server from ever shutting down.
23
+ *
24
+ * Also the one place every tool call passes through, so it is where the status
25
+ * page gets its record of what is running and what happened. Only the tool name
26
+ * is passed on, never the arguments: see activity.ts for why.
27
+ */
28
+ export async function track(fn, tool) {
29
+ inFlight++;
30
+ const record = tool === undefined ? undefined : activity.begin(tool);
31
+ let failed = false;
32
+ try {
33
+ return await fn();
34
+ }
35
+ catch (error) {
36
+ failed = true;
37
+ throw error;
38
+ }
39
+ finally {
40
+ if (record !== undefined)
41
+ activity.finish(record, failed ? 'failed' : 'ok');
42
+ inFlight--;
43
+ if (inFlight === 0) {
44
+ const waiting = idleResolvers;
45
+ idleResolvers = [];
46
+ for (const resolve of waiting)
47
+ resolve();
48
+ }
49
+ }
50
+ }
51
+ /**
52
+ * Waits until no call is running any more, but at most `maxMs`.
53
+ *
54
+ * Returns true when things really went quiet and false when the deadline
55
+ * passed. The return value lets the caller record that it gave up instead of
56
+ * hiding it.
57
+ */
58
+ export function waitForIdle(maxMs = 5000) {
59
+ if (inFlight === 0)
60
+ return Promise.resolve(true);
61
+ return new Promise((resolve) => {
62
+ const timer = setTimeout(() => {
63
+ idleResolvers = idleResolvers.filter((f) => f !== onIdle);
64
+ resolve(false);
65
+ }, maxMs);
66
+ const onIdle = () => {
67
+ clearTimeout(timer);
68
+ resolve(true);
69
+ };
70
+ idleResolvers.push(onIdle);
71
+ });
72
+ }
73
+ /** Resets the state. For tests only. */
74
+ export function _reset() {
75
+ inFlight = 0;
76
+ idleResolvers = [];
77
+ }
78
+ //# sourceMappingURL=in-flight.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"in-flight.js","sourceRoot":"","sources":["../src/in-flight.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAA;AAEzC,IAAI,QAAQ,GAAG,CAAC,CAAA;AAChB,IAAI,aAAa,GAAsB,EAAE,CAAA;AAEzC,4EAA4E;AAC5E,MAAM,UAAU,aAAa;IAC3B,OAAO,QAAQ,CAAA;AACjB,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,KAAK,CAAI,EAAoB,EAAE,IAAa;IAChE,QAAQ,EAAE,CAAA;IACV,MAAM,MAAM,GAAG,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IACpE,IAAI,MAAM,GAAG,KAAK,CAAA;IAClB,IAAI,CAAC;QACH,OAAO,MAAM,EAAE,EAAE,CAAA;IACnB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,GAAG,IAAI,CAAA;QACb,MAAM,KAAK,CAAA;IACb,CAAC;YAAS,CAAC;QACT,IAAI,MAAM,KAAK,SAAS;YAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;QAC3E,QAAQ,EAAE,CAAA;QACV,IAAI,QAAQ,KAAK,CAAC,EAAE,CAAC;YACnB,MAAM,OAAO,GAAG,aAAa,CAAA;YAC7B,aAAa,GAAG,EAAE,CAAA;YAClB,KAAK,MAAM,OAAO,IAAI,OAAO;gBAAE,OAAO,EAAE,CAAA;QAC1C,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,KAAK,GAAG,IAAI;IACtC,IAAI,QAAQ,KAAK,CAAC;QAAE,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IAEhD,OAAO,IAAI,OAAO,CAAU,CAAC,OAAO,EAAE,EAAE;QACtC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YAC5B,aAAa,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,MAAM,CAAC,CAAA;YACzD,OAAO,CAAC,KAAK,CAAC,CAAA;QAChB,CAAC,EAAE,KAAK,CAAC,CAAA;QAET,MAAM,MAAM,GAAG,GAAG,EAAE;YAClB,YAAY,CAAC,KAAK,CAAC,CAAA;YACnB,OAAO,CAAC,IAAI,CAAC,CAAA;QACf,CAAC,CAAA;QACD,aAAa,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IAC5B,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,wCAAwC;AACxC,MAAM,UAAU,MAAM;IACpB,QAAQ,GAAG,CAAC,CAAA;IACZ,aAAa,GAAG,EAAE,CAAA;AACpB,CAAC"}
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Moving messages, read state and trash.
3
+ *
4
+ * Measured against the Bridge on 31.07.2026, and almost every guard in this
5
+ * file exists because a measurement contradicted the obvious expectation:
6
+ *
7
+ * 1. **`MOVE` reports a refusal as the return value `false`, not as an error.**
8
+ * Into a mailbox that does not exist it returns `false` and changes nothing.
9
+ * Code that ignores the return value reports success for an operation that
10
+ * never happened. That is the opposite of `COPY`, which reports success and
11
+ * does nothing (see labels.ts), so neither call can be trusted the same way.
12
+ * 2. **`MOVE` out of "All Mail" returns `false` and changes nothing.** This is
13
+ * the important one: "All Mail" is where resolveMessageId looks when no
14
+ * mailbox is named, so the obvious implementation would fail silently in the
15
+ * ordinary case. Moving needs the folder the message really sits in, which
16
+ * is why this module has its own lookup.
17
+ * 3. **`MOVE` into a label is not a move.** The label is applied and the message
18
+ * stays in its folder, exactly as a `COPY` there would. Refused here, with a
19
+ * pointer at add_label, because a tool called "move" must not quietly do
20
+ * something else.
21
+ * 4. **A move takes about 15 seconds to settle**, and during that window the
22
+ * Bridge shows the message in both places. Nothing here verifies its own
23
+ * result, and the answers say so.
24
+ * 5. **Flag changes are immediate and hold**, unlike everything else in this
25
+ * file. \Seen and \Flagged were correct straight away and unchanged 20
26
+ * seconds later, so setting flags gets no delay warning.
27
+ *
28
+ * Nothing in this module deletes. There is no expunge here and no tool that
29
+ * removes a message for good: trash is a move to the Trash mailbox, and Proton
30
+ * keeps it in "All Mail", which the measurement confirmed.
31
+ */
32
+ import type { Connection, Mailbox } from '../bridge/connection.js';
33
+ /**
34
+ * How many messages one call may touch.
35
+ *
36
+ * A bound rather than a preference: every message costs a lookup and a write,
37
+ * and a model that miscounts should not be able to reorganise a mailbox in one
38
+ * call before anyone can read the answer.
39
+ */
40
+ export declare const MAX_BATCH = 50;
41
+ /**
42
+ * Checks that a destination is a place a message can actually be moved to.
43
+ *
44
+ * Deliberately does not list the folders that do exist. A failure message
45
+ * naming them would put the account's folder structure into the answer without
46
+ * anyone asking for it, and folder names alone can name a bank or an employer.
47
+ * list_folders is where that belongs.
48
+ */
49
+ export declare function assertMoveTarget(target: string, mailboxes: Mailbox[]): string;
50
+ /**
51
+ * Finds the folder a message actually sits in.
52
+ *
53
+ * Not resolveMessageId, and that is the whole point. resolveMessageId answers
54
+ * "where can I read this", and its fallback is "All Mail", which holds
55
+ * everything. Measured: a MOVE issued from inside "All Mail" returns false and
56
+ * changes nothing, so for a move that answer is worse than useless.
57
+ *
58
+ * `hint` is worth passing. Without it this walks the mailboxes a message can
59
+ * live in and searches each one, which is a request per mailbox. Listings name
60
+ * the mailbox they came from, so a caller almost always has it.
61
+ */
62
+ export declare function findHomeFolder(connection: Connection, rawId: string, hint?: string): Promise<{
63
+ messageId: string;
64
+ path: string;
65
+ uid: number;
66
+ }>;
67
+ /** What happened to one message of a batch. */
68
+ export interface MessageOutcome {
69
+ messageId: string;
70
+ ok: boolean;
71
+ /** The folder it came from, when it was found. */
72
+ from?: string;
73
+ /** Why it did not work, in a form the caller can act on. */
74
+ reason?: string;
75
+ }
76
+ export interface BatchResult {
77
+ outcomes: MessageOutcome[];
78
+ succeeded: number;
79
+ failed: number;
80
+ }
81
+ /**
82
+ * Moves messages into a folder.
83
+ *
84
+ * One message's failure does not stop the rest: a batch that gives up halfway
85
+ * leaves the caller unable to say what happened, which is worse than a partial
86
+ * result that is reported honestly.
87
+ */
88
+ export declare function moveMessages(connection: Connection, readOnly: boolean, messageIds: string[], target: string, hint?: string): Promise<BatchResult & {
89
+ target: string;
90
+ }>;
91
+ /** Which flags a call wants changed. Absent means "leave alone". */
92
+ export interface FlagChanges {
93
+ /** True marks as read, false as unread. */
94
+ read?: boolean;
95
+ /** Proton shows this as the star. */
96
+ starred?: boolean;
97
+ }
98
+ /**
99
+ * Sets read state and star.
100
+ *
101
+ * Unlike everything else here this needs no settling time: measured, the flags
102
+ * were correct immediately and unchanged twenty seconds later.
103
+ */
104
+ export declare function setFlags(connection: Connection, readOnly: boolean, messageIds: string[], changes: FlagChanges, hint?: string): Promise<BatchResult>;
105
+ /** The mailbox Proton puts discarded messages in. Nothing here empties it. */
106
+ export declare const TRASH = "Trash";
107
+ /**
108
+ * Moves messages to the trash.
109
+ *
110
+ * A move and nothing else. There is no permanent deletion in this server, on
111
+ * purpose: a wrong decision by a model has to stay reversible, and measured,
112
+ * a message in the trash is still in "All Mail" and can be moved back out.
113
+ */
114
+ export declare function trashMessages(connection: Connection, readOnly: boolean, messageIds: string[], hint?: string): Promise<BatchResult & {
115
+ target: string;
116
+ }>;
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Moving messages, read state and trash.
3
+ *
4
+ * Measured against the Bridge on 31.07.2026, and almost every guard in this
5
+ * file exists because a measurement contradicted the obvious expectation:
6
+ *
7
+ * 1. **`MOVE` reports a refusal as the return value `false`, not as an error.**
8
+ * Into a mailbox that does not exist it returns `false` and changes nothing.
9
+ * Code that ignores the return value reports success for an operation that
10
+ * never happened. That is the opposite of `COPY`, which reports success and
11
+ * does nothing (see labels.ts), so neither call can be trusted the same way.
12
+ * 2. **`MOVE` out of "All Mail" returns `false` and changes nothing.** This is
13
+ * the important one: "All Mail" is where resolveMessageId looks when no
14
+ * mailbox is named, so the obvious implementation would fail silently in the
15
+ * ordinary case. Moving needs the folder the message really sits in, which
16
+ * is why this module has its own lookup.
17
+ * 3. **`MOVE` into a label is not a move.** The label is applied and the message
18
+ * stays in its folder, exactly as a `COPY` there would. Refused here, with a
19
+ * pointer at add_label, because a tool called "move" must not quietly do
20
+ * something else.
21
+ * 4. **A move takes about 15 seconds to settle**, and during that window the
22
+ * Bridge shows the message in both places. Nothing here verifies its own
23
+ * result, and the answers say so.
24
+ * 5. **Flag changes are immediate and hold**, unlike everything else in this
25
+ * file. \Seen and \Flagged were correct straight away and unchanged 20
26
+ * seconds later, so setting flags gets no delay warning.
27
+ *
28
+ * Nothing in this module deletes. There is no expunge here and no tool that
29
+ * removes a message for good: trash is a move to the Trash mailbox, and Proton
30
+ * keeps it in "All Mail", which the measurement confirmed.
31
+ */
32
+ import { BridgeError } from '../bridge/errors.js';
33
+ import { normaliseMessageId, ALL_MAIL } from './ids.js';
34
+ import { LABEL_PREFIX } from './labels.js';
35
+ /**
36
+ * How many messages one call may touch.
37
+ *
38
+ * A bound rather than a preference: every message costs a lookup and a write,
39
+ * and a model that miscounts should not be able to reorganise a mailbox in one
40
+ * call before anyone can read the answer.
41
+ */
42
+ export const MAX_BATCH = 50;
43
+ /** Views rather than places. A message cannot be moved into one of these. */
44
+ const VIRTUAL = new Set([ALL_MAIL, 'Starred']);
45
+ /** Refuses every write while the server runs read-only. */
46
+ function assertWritable(readOnly, what) {
47
+ if (readOnly) {
48
+ throw new BridgeError(`The server is running read-only, so ${what} is refused. ` +
49
+ 'This is set by PROTON_MCP_READ_ONLY and the web interface reports it as the current mode.');
50
+ }
51
+ }
52
+ /**
53
+ * Checks that a destination is a place a message can actually be moved to.
54
+ *
55
+ * Deliberately does not list the folders that do exist. A failure message
56
+ * naming them would put the account's folder structure into the answer without
57
+ * anyone asking for it, and folder names alone can name a bank or an employer.
58
+ * list_folders is where that belongs.
59
+ */
60
+ export function assertMoveTarget(target, mailboxes) {
61
+ const path = target.trim();
62
+ if (!path)
63
+ throw new BridgeError('An empty mailbox name cannot be used as a destination.');
64
+ if (path.startsWith(LABEL_PREFIX) || path === 'Labels') {
65
+ throw new BridgeError(`"${path}" is a label, not a folder. Measured against the Bridge: moving a message into a ` +
66
+ 'label applies the label and leaves the message in its folder, so this would not move ' +
67
+ 'anything. Use add_label to apply a label, and name a folder here.');
68
+ }
69
+ if (VIRTUAL.has(path)) {
70
+ throw new BridgeError(`"${path}" is a view of other mailboxes rather than a place to put a message. ` +
71
+ (path === 'Starred'
72
+ ? 'Use set_flags with starred to star a message.'
73
+ : `"${ALL_MAIL}" holds every message already, whatever folder it is in.`));
74
+ }
75
+ const box = mailboxes.find((b) => b.path === path);
76
+ if (!box) {
77
+ throw new BridgeError(`There is no mailbox "${path}". Use list_folders to see the available paths; user folders ` +
78
+ 'live below "Folders/". Note that the Bridge accepts a move into a mailbox that is not ' +
79
+ 'there without moving anything, so this is checked before the call rather than after it.');
80
+ }
81
+ if (!box.selectable) {
82
+ throw new BridgeError(`"${path}" is the container the folders live in, not a folder itself. It cannot hold ` +
83
+ 'messages. Name one of the folders below it.');
84
+ }
85
+ return path;
86
+ }
87
+ /**
88
+ * Finds the folder a message actually sits in.
89
+ *
90
+ * Not resolveMessageId, and that is the whole point. resolveMessageId answers
91
+ * "where can I read this", and its fallback is "All Mail", which holds
92
+ * everything. Measured: a MOVE issued from inside "All Mail" returns false and
93
+ * changes nothing, so for a move that answer is worse than useless.
94
+ *
95
+ * `hint` is worth passing. Without it this walks the mailboxes a message can
96
+ * live in and searches each one, which is a request per mailbox. Listings name
97
+ * the mailbox they came from, so a caller almost always has it.
98
+ */
99
+ export async function findHomeFolder(connection, rawId, hint) {
100
+ const messageId = normaliseMessageId(rawId);
101
+ const lookIn = async (path) => {
102
+ const found = await connection
103
+ .withMailbox(path, async (client, status) => {
104
+ // The Bridge answers a fetch on an empty mailbox with BAD.
105
+ if (status.messages === 0)
106
+ return undefined;
107
+ const hits = await client.search({ header: { 'message-id': messageId } }, { uid: true });
108
+ if (!Array.isArray(hits) || hits.length === 0)
109
+ return undefined;
110
+ return Math.max(...hits);
111
+ })
112
+ .catch(() => undefined);
113
+ return found;
114
+ };
115
+ if (hint && !VIRTUAL.has(hint) && !hint.startsWith(LABEL_PREFIX)) {
116
+ const uid = await lookIn(hint);
117
+ if (uid !== undefined)
118
+ return { messageId, path: hint, uid };
119
+ }
120
+ const mailboxes = await connection.listMailboxes();
121
+ // A message lives in exactly one folder. Labels and the two views are not
122
+ // folders, so searching them would only produce a location a move cannot use.
123
+ const candidates = mailboxes
124
+ .filter((b) => b.selectable && b.kind !== 'label' && !VIRTUAL.has(b.path))
125
+ .map((b) => b.path)
126
+ // The common ones first, so the usual case stops early.
127
+ .sort((a, b) => rank(a) - rank(b));
128
+ for (const path of candidates) {
129
+ if (path === hint)
130
+ continue;
131
+ const uid = await lookIn(path);
132
+ if (uid !== undefined)
133
+ return { messageId, path, uid };
134
+ }
135
+ throw new BridgeError(`No folder holds a message with the id ${messageId}. It may have been deleted, or the id may ` +
136
+ 'be incomplete. Identifiers come from list_messages or search_messages and have to be ' +
137
+ 'passed on unchanged.');
138
+ }
139
+ /** Where to look first. Ordinary reading happens in the top few. */
140
+ function rank(path) {
141
+ const order = ['INBOX', 'Archive', 'Sent', 'Drafts', 'Spam', 'Trash'];
142
+ const known = order.indexOf(path);
143
+ if (known >= 0)
144
+ return known;
145
+ // User folders before the remaining system ones.
146
+ return path.startsWith('Folders/') ? 10 : 20;
147
+ }
148
+ function summarise(outcomes) {
149
+ const succeeded = outcomes.filter((o) => o.ok).length;
150
+ return { outcomes, succeeded, failed: outcomes.length - succeeded };
151
+ }
152
+ function assertBatch(ids) {
153
+ if (ids.length === 0)
154
+ throw new BridgeError('No message id was given, so there is nothing to do.');
155
+ if (ids.length > MAX_BATCH) {
156
+ throw new BridgeError(`${ids.length} messages were given, and at most ${MAX_BATCH} are accepted in one call. ` +
157
+ 'Split the work into several calls, so that a mistake stays small enough to read.');
158
+ }
159
+ }
160
+ /**
161
+ * Moves messages into a folder.
162
+ *
163
+ * One message's failure does not stop the rest: a batch that gives up halfway
164
+ * leaves the caller unable to say what happened, which is worse than a partial
165
+ * result that is reported honestly.
166
+ */
167
+ export async function moveMessages(connection, readOnly, messageIds, target, hint) {
168
+ assertWritable(readOnly, 'moving messages');
169
+ assertBatch(messageIds);
170
+ const mailboxes = await connection.listMailboxes();
171
+ const path = assertMoveTarget(target, mailboxes);
172
+ const outcomes = [];
173
+ for (const raw of messageIds) {
174
+ try {
175
+ const home = await findHomeFolder(connection, raw, hint);
176
+ if (home.path === path) {
177
+ outcomes.push({
178
+ messageId: home.messageId,
179
+ ok: true,
180
+ from: home.path,
181
+ reason: 'was already there, nothing was moved',
182
+ });
183
+ continue;
184
+ }
185
+ const accepted = await connection.withMailbox(home.path, async (client) => client.messageMove(String(home.uid), path, { uid: true }));
186
+ // Measured: a refused move is a falsy return value, never an exception.
187
+ // Treating the call as successful because it did not throw would report a
188
+ // move that did not happen.
189
+ if (!accepted) {
190
+ outcomes.push({
191
+ messageId: home.messageId,
192
+ ok: false,
193
+ from: home.path,
194
+ reason: `the Bridge refused the move from "${home.path}" to "${path}" without giving a reason`,
195
+ });
196
+ continue;
197
+ }
198
+ outcomes.push({ messageId: home.messageId, ok: true, from: home.path });
199
+ }
200
+ catch (error) {
201
+ outcomes.push({
202
+ messageId: raw,
203
+ ok: false,
204
+ reason: error instanceof BridgeError ? error.message : String(error),
205
+ });
206
+ }
207
+ }
208
+ return { ...summarise(outcomes), target: path };
209
+ }
210
+ /**
211
+ * Sets read state and star.
212
+ *
213
+ * Unlike everything else here this needs no settling time: measured, the flags
214
+ * were correct immediately and unchanged twenty seconds later.
215
+ */
216
+ export async function setFlags(connection, readOnly, messageIds, changes, hint) {
217
+ assertWritable(readOnly, 'changing the read state or the star');
218
+ assertBatch(messageIds);
219
+ if (changes.read === undefined && changes.starred === undefined) {
220
+ throw new BridgeError('Neither the read state nor the star was given, so there is nothing to change.');
221
+ }
222
+ const add = [];
223
+ const remove = [];
224
+ if (changes.read !== undefined)
225
+ (changes.read ? add : remove).push('\\Seen');
226
+ if (changes.starred !== undefined)
227
+ (changes.starred ? add : remove).push('\\Flagged');
228
+ const outcomes = [];
229
+ for (const raw of messageIds) {
230
+ try {
231
+ // Flags belong to a mailbox, so this needs the folder as well: setting a
232
+ // flag on the "All Mail" copy would leave the folder's own view alone.
233
+ const home = await findHomeFolder(connection, raw, hint);
234
+ await connection.withMailbox(home.path, async (client) => {
235
+ const uid = String(home.uid);
236
+ if (add.length)
237
+ await client.messageFlagsAdd(uid, add, { uid: true });
238
+ if (remove.length)
239
+ await client.messageFlagsRemove(uid, remove, { uid: true });
240
+ });
241
+ outcomes.push({ messageId: home.messageId, ok: true, from: home.path });
242
+ }
243
+ catch (error) {
244
+ outcomes.push({
245
+ messageId: raw,
246
+ ok: false,
247
+ reason: error instanceof BridgeError ? error.message : String(error),
248
+ });
249
+ }
250
+ }
251
+ return summarise(outcomes);
252
+ }
253
+ /** The mailbox Proton puts discarded messages in. Nothing here empties it. */
254
+ export const TRASH = 'Trash';
255
+ /**
256
+ * Moves messages to the trash.
257
+ *
258
+ * A move and nothing else. There is no permanent deletion in this server, on
259
+ * purpose: a wrong decision by a model has to stay reversible, and measured,
260
+ * a message in the trash is still in "All Mail" and can be moved back out.
261
+ */
262
+ export async function trashMessages(connection, readOnly, messageIds, hint) {
263
+ return moveMessages(connection, readOnly, messageIds, TRASH, hint);
264
+ }
265
+ //# sourceMappingURL=actions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"actions.js","sourceRoot":"","sources":["../../src/mail/actions.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Reading attachments.
3
+ *
4
+ * Attachments are the fastest way to blow a context window: the largest message
5
+ * in a measured sample was 4.8 MB. Nothing is fetched unless asked for by
6
+ * index, and text is subject to a budget.
7
+ *
8
+ * Binary content is deliberately not returned yet. Handing it over would mean
9
+ * either base64 in the context, which is unusable, or writing decrypted content
10
+ * to disk. The latter would undo Proton's encryption locally and needs a
11
+ * deliberate decision first.
12
+ */
13
+ import type { Connection } from '../bridge/connection.js';
14
+ /** Upper bound for attachment text. Roughly 12000 tokens. */
15
+ export declare const MAX_ATTACHMENT_CHARS = 48000;
16
+ export declare function isTextual(contentType: string): boolean;
17
+ export interface AttachmentContent {
18
+ filename: string;
19
+ contentType: string;
20
+ size: number;
21
+ /** Present for textual attachments. */
22
+ text?: string;
23
+ truncated?: boolean;
24
+ /** Where to continue reading, or undefined when the end was reached. */
25
+ nextOffset?: number;
26
+ totalChars?: number;
27
+ /** Set when the content cannot be handed over as text. */
28
+ unsupportedReason?: string;
29
+ }
30
+ /**
31
+ * Reads one attachment of a message by index.
32
+ *
33
+ * The index refers to the list from get_message, which already excludes
34
+ * Proton's own public key. The same filter runs here so both stay aligned.
35
+ */
36
+ export declare function getAttachment(connection: Connection, messageId: string, index: number, options?: {
37
+ hint?: string;
38
+ offset?: number;
39
+ }): Promise<AttachmentContent>;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Reading attachments.
3
+ *
4
+ * Attachments are the fastest way to blow a context window: the largest message
5
+ * in a measured sample was 4.8 MB. Nothing is fetched unless asked for by
6
+ * index, and text is subject to a budget.
7
+ *
8
+ * Binary content is deliberately not returned yet. Handing it over would mean
9
+ * either base64 in the context, which is unusable, or writing decrypted content
10
+ * to disk. The latter would undo Proton's encryption locally and needs a
11
+ * deliberate decision first.
12
+ */
13
+ import { simpleParser } from 'mailparser';
14
+ import { BridgeError } from '../bridge/errors.js';
15
+ import { resolveMessageId } from './ids.js';
16
+ import { isProtonPublicKey, truncate } from '../mime/parse.js';
17
+ /** Upper bound for attachment text. Roughly 12000 tokens. */
18
+ export const MAX_ATTACHMENT_CHARS = 48_000;
19
+ /** Content types that can be shown as text. */
20
+ const TEXTUAL = [
21
+ /^text\//i,
22
+ /^application\/json$/i,
23
+ /^application\/xml$/i,
24
+ /^application\/x-yaml$/i,
25
+ /^application\/javascript$/i,
26
+ /\+xml$/i,
27
+ /\+json$/i,
28
+ ];
29
+ export function isTextual(contentType) {
30
+ return TEXTUAL.some((p) => p.test(contentType));
31
+ }
32
+ /**
33
+ * Reads one attachment of a message by index.
34
+ *
35
+ * The index refers to the list from get_message, which already excludes
36
+ * Proton's own public key. The same filter runs here so both stay aligned.
37
+ */
38
+ export async function getAttachment(connection, messageId, index, options = {}) {
39
+ if (!Number.isInteger(index) || index < 0) {
40
+ throw new BridgeError(`"${index}" is not a valid attachment index. Expected 0 or higher.`);
41
+ }
42
+ const resolved = await resolveMessageId(connection, messageId, options.hint);
43
+ const source = await connection.withMailbox(resolved.path, async (client) => {
44
+ const message = await client.fetchOne(String(resolved.uid), { source: true }, { uid: true });
45
+ return message && message.source ? message.source : undefined;
46
+ });
47
+ if (!source) {
48
+ throw new BridgeError(`The message ${resolved.messageId} could not be read from "${resolved.path}". It may have been moved.`);
49
+ }
50
+ const parsed = await simpleParser(source, { skipTextToHtml: true, skipTextLinks: true });
51
+ const real = (parsed.attachments ?? []).filter((a) => !isProtonPublicKey(a.contentType ?? '', a.filename ?? ''));
52
+ const wanted = real[index];
53
+ if (!wanted) {
54
+ const available = real.length;
55
+ throw new BridgeError(available === 0
56
+ ? `The message ${resolved.messageId} has no attachments.`
57
+ : `Attachment index ${index} does not exist. The message has ${available}, so valid indices are 0 to ${available - 1}.`);
58
+ }
59
+ const contentType = wanted.contentType ?? 'application/octet-stream';
60
+ const filename = wanted.filename ?? `attachment-${index}`;
61
+ const size = wanted.size ?? wanted.content?.length ?? 0;
62
+ if (!isTextual(contentType)) {
63
+ return {
64
+ filename,
65
+ contentType,
66
+ size,
67
+ unsupportedReason: `This attachment is ${contentType} and cannot be returned as text. ` +
68
+ 'Binary attachments are not supported yet, because handing them over would mean either ' +
69
+ 'base64 in the context or writing decrypted content to disk. Open the message in a mail client instead.',
70
+ };
71
+ }
72
+ const raw = wanted.content?.toString('utf8') ?? '';
73
+ const excerpt = truncate(raw, MAX_ATTACHMENT_CHARS, options.offset ?? 0);
74
+ const result = {
75
+ filename,
76
+ contentType,
77
+ size,
78
+ text: excerpt.text,
79
+ truncated: excerpt.truncated,
80
+ totalChars: excerpt.totalChars,
81
+ };
82
+ if (excerpt.nextOffset !== undefined)
83
+ result.nextOffset = excerpt.nextOffset;
84
+ return result;
85
+ }
86
+ //# sourceMappingURL=attachments.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachments.js","sourceRoot":"","sources":["../../src/mail/attachments.ts"],"names":[],"mappings":"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"}