@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,265 @@
1
+ /**
2
+ * Which markup a message may contain, and what has to be shown before it is sent.
3
+ *
4
+ * ## The measurement this is built on
5
+ *
6
+ * The obvious worry about formatted mail is CSS that hides text, and it is the
7
+ * wrong worry. Measured on 31.07.2026 against this server's own conversion:
8
+ * `display:none`, `font-size:0` and white-on-white all appear in the text
9
+ * preview, because the conversion reads the markup and does not apply styling.
10
+ * The preview therefore shows *more* than the recipient will see, which is the
11
+ * harmless direction: a person confirming reads everything.
12
+ *
13
+ * What is dangerous is the other direction, where the preview shows **less**
14
+ * than the recipient reads, and the same measurement found it:
15
+ *
16
+ * | Written | In the preview | What the recipient sees |
17
+ * | :--- | :--- | :--- |
18
+ * | `<img src="..." alt="TEXT">` | **nothing** | the alt text, because clients block remote images by default |
19
+ * | `<span title="TEXT">` | nothing | the title on hover |
20
+ * | `<style>p::before{content:"TEXT"}</style>` | nothing | generated text, in clients that keep style blocks |
21
+ * | `<noscript>`, `<textarea>` | the text | nothing, or something else |
22
+ *
23
+ * The first row is a working way past the confirmation. An assistant that has
24
+ * read a stranger's message could compose an image whose alt text carries the
25
+ * request, and the person confirming would see an empty message. Remote images
26
+ * are blocked by default in Proton and elsewhere, so the alt text is not a
27
+ * fallback, it is what actually arrives.
28
+ *
29
+ * Two conclusions, and they shape everything below:
30
+ *
31
+ * 1. **A list of what is permitted, not a filter for what is not.** `<style>`,
32
+ * `<script>`, `<noscript>`, `<textarea>` and `<head>` are absent from the
33
+ * list, so they are refused without anyone having had to think of them. That
34
+ * is the whole argument for a positive list: what nobody anticipated does not
35
+ * get through.
36
+ * 2. **Every piece of text a recipient can read is put in front of the person
37
+ * confirming**, including the ones the preview drops: alt text, titles, and
38
+ * every address behind a link or an image.
39
+ *
40
+ * The hiding properties are refused as well. Not because they defeat the
41
+ * preview, measured they do not, but because they let a message be delivered
42
+ * looking different from how it was written, and nothing is lost by saying no.
43
+ */
44
+ import { Parser } from 'htmlparser2';
45
+ import { BridgeError } from '../bridge/errors.js';
46
+ /**
47
+ * The elements a message may use.
48
+ *
49
+ * Absent by design and worth naming, because the reason is the measurement
50
+ * above rather than taste: `style`, `script`, `noscript`, `textarea`, `head`,
51
+ * `title`, `meta`, `link`, `iframe`, `object`, `form`, `input`, `button`.
52
+ */
53
+ export const PERMITTED_TAGS = new Set([
54
+ 'html', 'body',
55
+ 'h1', 'h2', 'h3', 'h4', 'p', 'br', 'hr', 'blockquote', 'div', 'span',
56
+ 'b', 'strong', 'i', 'em', 'u', 's', 'code', 'pre', 'small', 'sub', 'sup',
57
+ 'ul', 'ol', 'li', 'dl', 'dt', 'dd',
58
+ 'table', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td', 'caption',
59
+ 'a', 'img',
60
+ ]);
61
+ /** Attributes each element may carry, on top of the ones allowed everywhere. */
62
+ const PERMITTED_ATTRIBUTES = {
63
+ '*': ['style', 'title', 'dir', 'lang'],
64
+ a: ['href', 'target', 'rel'],
65
+ img: ['src', 'alt', 'width', 'height'],
66
+ // height sits beside width rather than being an omission with a reason. A
67
+ // table cell of a given height hides nothing: its content still renders, and
68
+ // the properties that do hide are refused as styles further down.
69
+ table: ['width', 'height', 'border', 'cellpadding', 'cellspacing', 'align'],
70
+ td: ['colspan', 'rowspan', 'align', 'valign', 'width', 'height'],
71
+ th: ['colspan', 'rowspan', 'align', 'valign', 'width', 'height'],
72
+ tr: ['align', 'valign', 'height'],
73
+ ol: ['start', 'type'],
74
+ blockquote: ['cite'],
75
+ };
76
+ /** Style properties a message may set. */
77
+ export const PERMITTED_STYLES = new Set([
78
+ 'color', 'background', 'background-color',
79
+ 'font', 'font-family', 'font-size', 'font-style', 'font-weight', 'font-variant',
80
+ 'text-align', 'text-decoration', 'text-transform', 'line-height', 'letter-spacing',
81
+ 'padding', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left',
82
+ 'margin', 'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
83
+ 'border', 'border-top', 'border-right', 'border-bottom', 'border-left',
84
+ 'border-color', 'border-style', 'border-width', 'border-radius', 'border-collapse',
85
+ 'width', 'height', 'max-width', 'vertical-align', 'white-space', 'list-style',
86
+ ]);
87
+ /**
88
+ * Properties that take a message out of sight without taking it out of the
89
+ * message. Refused whatever else is written.
90
+ */
91
+ const HIDING_STYLES = new Set([
92
+ 'display', 'visibility', 'opacity', 'position', 'z-index', 'overflow',
93
+ 'clip', 'clip-path', 'transform', 'text-indent', 'float', 'content',
94
+ 'max-height', 'min-height', 'mix-blend-mode', 'filter',
95
+ ]);
96
+ /** Properties whose value must be a positive size. Zero is a way of hiding. */
97
+ const MUST_BE_POSITIVE = new Set(['font-size', 'line-height', 'width', 'height', 'max-width']);
98
+ /** A scheme in an href or src. Anything else is refused. */
99
+ const PERMITTED_SCHEMES = ['http:', 'https:', 'mailto:', 'cid:'];
100
+ function styleProblems(tag, style) {
101
+ const problems = [];
102
+ for (const declaration of style.split(';')) {
103
+ const [rawName, ...rest] = declaration.split(':');
104
+ const name = (rawName ?? '').trim().toLowerCase();
105
+ const value = rest.join(':').trim();
106
+ if (!name)
107
+ continue;
108
+ if (HIDING_STYLES.has(name)) {
109
+ problems.push({
110
+ found: `style "${name}" on <${tag}>`,
111
+ reason: `"${name}" can put text out of sight while leaving it in the message, so a recipient ` +
112
+ 'and the person who confirmed the send would not read the same thing. Style the message ' +
113
+ 'with colour, font and spacing instead.',
114
+ });
115
+ continue;
116
+ }
117
+ if (!PERMITTED_STYLES.has(name)) {
118
+ problems.push({
119
+ found: `style "${name}" on <${tag}>`,
120
+ reason: `"${name}" is not among the properties this server sends. Permitted are colour, ` +
121
+ 'background, font, alignment, spacing, borders and size.',
122
+ });
123
+ continue;
124
+ }
125
+ if (MUST_BE_POSITIVE.has(name) && /^0(\D|$)|^-/.test(value)) {
126
+ problems.push({
127
+ found: `style "${name}: ${value}" on <${tag}>`,
128
+ reason: `A "${name}" of zero or less makes text unreadable without removing it, which is the ` +
129
+ 'same as hiding it. Give it a real size or leave it out.',
130
+ });
131
+ }
132
+ }
133
+ return problems;
134
+ }
135
+ function urlProblem(tag, attribute, value) {
136
+ const trimmed = value.trim();
137
+ // A relative address has no meaning in a message: there is no page it is
138
+ // relative to, so it would simply be broken wherever it arrived.
139
+ if (!/^[a-z][a-z0-9+.-]*:/i.test(trimmed)) {
140
+ return {
141
+ found: `${attribute}="${trimmed}" on <${tag}>`,
142
+ reason: 'An address in a message has to be complete. A relative one has nothing to be relative ' +
143
+ 'to once the message has been delivered. Write the whole address, including https://.',
144
+ };
145
+ }
146
+ const scheme = trimmed.slice(0, trimmed.indexOf(':') + 1).toLowerCase();
147
+ if (!PERMITTED_SCHEMES.includes(scheme)) {
148
+ return {
149
+ found: `${attribute}="${trimmed.slice(0, 40)}" on <${tag}>`,
150
+ reason: `"${scheme}" is not an address this server puts in a message. Permitted are ` +
151
+ `${PERMITTED_SCHEMES.join(', ')}. A "data:" address in particular would carry a whole ` +
152
+ 'file inside the markup, where nothing lists it as an attachment.',
153
+ };
154
+ }
155
+ return undefined;
156
+ }
157
+ /**
158
+ * Reads a message's markup: what is wrong with it, and what has to be shown.
159
+ *
160
+ * One pass, because the two questions are the same walk over the document and
161
+ * answering them separately is how they drift apart.
162
+ */
163
+ export function readMarkup(html) {
164
+ const problems = [];
165
+ const urls = [];
166
+ const hiddenText = [];
167
+ const contentIds = [];
168
+ // A link's text arrives as separate events between its tags, so it is
169
+ // collected while the link is open.
170
+ let openLink;
171
+ const parser = new Parser({
172
+ onopentag(name, attributes) {
173
+ const tag = name.toLowerCase();
174
+ if (!PERMITTED_TAGS.has(tag)) {
175
+ problems.push({
176
+ found: `<${tag}>`,
177
+ reason: `<${tag}> is not among the elements this server sends. Permitted are headings, ` +
178
+ 'paragraphs, line breaks, rules, quotes, emphasis, lists, tables, links and images.',
179
+ });
180
+ return;
181
+ }
182
+ const allowed = [...(PERMITTED_ATTRIBUTES['*'] ?? []), ...(PERMITTED_ATTRIBUTES[tag] ?? [])];
183
+ for (const [rawAttribute, value] of Object.entries(attributes)) {
184
+ const attribute = rawAttribute.toLowerCase();
185
+ if (!allowed.includes(attribute)) {
186
+ problems.push({
187
+ found: `${attribute}="${String(value).slice(0, 30)}" on <${tag}>`,
188
+ reason: attribute.startsWith('on')
189
+ ? 'An event handler has no place in a message. No mail client runs it, and ' +
190
+ 'what it contains would never be seen by anyone confirming the send.'
191
+ : `<${tag}> may not carry "${attribute}" here.`,
192
+ });
193
+ continue;
194
+ }
195
+ if (attribute === 'style')
196
+ problems.push(...styleProblems(tag, String(value)));
197
+ if (attribute === 'href' || attribute === 'src') {
198
+ const problem = urlProblem(tag, attribute, String(value));
199
+ if (problem)
200
+ problems.push(problem);
201
+ }
202
+ // A title is read on hover and never appears in the text preview.
203
+ if (attribute === 'title' && String(value).trim()) {
204
+ hiddenText.push(`title of <${tag}>: ${String(value).trim()}`);
205
+ }
206
+ }
207
+ if (tag === 'a' && typeof attributes.href === 'string') {
208
+ openLink = { url: attributes.href.trim(), text: '' };
209
+ }
210
+ if (tag === 'img' && typeof attributes.src === 'string') {
211
+ const src = attributes.src.trim();
212
+ const alt = typeof attributes.alt === 'string' ? attributes.alt.trim() : '';
213
+ urls.push({ kind: 'image', url: src, label: alt });
214
+ if (src.toLowerCase().startsWith('cid:'))
215
+ contentIds.push(src.slice(4));
216
+ // The measurement that made this file necessary: an alt text is what
217
+ // the recipient reads whenever the image is not loaded, and clients
218
+ // do not load remote images by default. It is absent from the preview.
219
+ if (alt)
220
+ hiddenText.push(`alt text of an image: ${alt}`);
221
+ }
222
+ },
223
+ ontext(text) {
224
+ if (openLink)
225
+ openLink.text += text;
226
+ },
227
+ onclosetag(name) {
228
+ if (name.toLowerCase() === 'a' && openLink) {
229
+ urls.push({ kind: 'link', url: openLink.url, label: openLink.text.trim() });
230
+ openLink = undefined;
231
+ }
232
+ },
233
+ oncomment() {
234
+ // Dropped by every reader and by the preview alike, so it hides nothing
235
+ // and is not worth refusing over.
236
+ },
237
+ }, { decodeEntities: true, lowerCaseTags: true, lowerCaseAttributeNames: true });
238
+ parser.write(html);
239
+ parser.end();
240
+ if (openLink)
241
+ urls.push({ kind: 'link', url: openLink.url, label: openLink.text.trim() });
242
+ return { problems, urls, hiddenText, contentIds };
243
+ }
244
+ /**
245
+ * Refuses markup this server will not send.
246
+ *
247
+ * Refused rather than repaired, deliberately. Stripping what is not permitted
248
+ * would mean the message that goes out is not the message that was written, and
249
+ * nobody would find out: not the caller, whose formatting quietly vanishes, and
250
+ * not the person confirming, who has nothing to compare against.
251
+ */
252
+ export function assertSendableMarkup(html) {
253
+ const reading = readMarkup(html);
254
+ if (reading.problems.length === 0)
255
+ return reading;
256
+ const listed = reading.problems
257
+ .slice(0, 8)
258
+ .map((p) => ` - ${p.found}\n ${p.reason}`)
259
+ .join('\n');
260
+ const more = reading.problems.length > 8 ? `\n ...and ${reading.problems.length - 8} more.` : '';
261
+ throw new BridgeError(`This markup cannot be sent, so nothing was written or sent.\n\n${listed}${more}\n\n` +
262
+ 'Markup is refused rather than cleaned up: a message that was quietly altered is no longer ' +
263
+ 'the message anyone agreed to.');
264
+ }
265
+ //# sourceMappingURL=markup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markup.js","sourceRoot":"","sources":["../../src/mail/markup.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Listing and reading messages.
3
+ *
4
+ * The guiding constraint is the context window, not the network. A real
5
+ * mailbox has a median message size of roughly 16200 tokens raw, so twenty raw
6
+ * messages exceed a full window. Listings therefore carry headers only, and
7
+ * full text is fetched one message at a time.
8
+ */
9
+ import type { ImapFlow } from 'imapflow';
10
+ import type { Connection } from '../bridge/connection.js';
11
+ import { type ParsedMessage, type Address } from '../mime/parse.js';
12
+ /** Upper bound for one listing. Deliberately low, callers can page. */
13
+ export declare const MAX_LIST_LIMIT = 100;
14
+ /** Character budget for a single message body. Roughly 6000 tokens. */
15
+ export declare const DEFAULT_TEXT_BUDGET = 24000;
16
+ export interface MessageHeader {
17
+ messageId: string;
18
+ subject: string;
19
+ from: Address[];
20
+ to: Address[];
21
+ date: Date | undefined;
22
+ /** Size of the raw message in bytes. Indicates what fetching it would cost. */
23
+ size: number;
24
+ seen: boolean;
25
+ flagged: boolean;
26
+ answered: boolean;
27
+ draft: boolean;
28
+ /** Whether the message carries attachments other than Proton's own key. */
29
+ hasAttachments: boolean;
30
+ }
31
+ /**
32
+ * What the ordering step cost.
33
+ *
34
+ * Reported rather than hidden, because it is the one part of a listing whose
35
+ * price grows with the size of the mailbox. A caller that sees 2000 ms here
36
+ * knows why the answer was slow, and so does anyone deciding whether this needs
37
+ * a cache.
38
+ */
39
+ export interface OrderingCost {
40
+ /** How many message dates had to be read to establish the order. */
41
+ messages: number;
42
+ elapsedMs: number;
43
+ }
44
+ export interface ListResult {
45
+ path: string;
46
+ /** Total number of messages in the mailbox, independent of paging. */
47
+ total: number;
48
+ offset: number;
49
+ headers: MessageHeader[];
50
+ ordering: OrderingCost;
51
+ }
52
+ export declare function toAddresses(list: Array<{
53
+ name?: string;
54
+ address?: string;
55
+ }> | undefined): Address[];
56
+ /**
57
+ * Guesses from the body structure whether real attachments are present.
58
+ *
59
+ * Cheap, because it works off the structure the server already returns rather
60
+ * than parsing the message. Proton's own public key is excluded, otherwise
61
+ * every sent message would look like it had an attachment.
62
+ */
63
+ export declare function detectAttachments(node: unknown): boolean;
64
+ /** A message reduced to what it takes to put it in order. */
65
+ export interface OrderEntry {
66
+ uid: number;
67
+ date: Date | undefined;
68
+ }
69
+ /**
70
+ * Reads nothing but the date of each message.
71
+ *
72
+ * This is the cheapest fetch IMAP offers for the purpose: ENVELOPE and nothing
73
+ * else. In particular no BODYSTRUCTURE, which is the expensive part of a full
74
+ * header fetch because the server has to walk the MIME tree to produce it.
75
+ *
76
+ * It runs over the entire result set, not over one page, and that is the point:
77
+ * an order can only be established from all of it. What it costs is reported
78
+ * back to the caller rather than swallowed.
79
+ *
80
+ * The date is taken from the envelope, which is the `Date:` field the sender
81
+ * wrote. INTERNALDATE would be cheaper still, but it is the time the server
82
+ * received the message and a copy between mailboxes may set it afresh, which is
83
+ * the very thing this ordering exists to survive.
84
+ */
85
+ export declare function fetchDates(client: ImapFlow, uids: number[]): Promise<OrderEntry[]>;
86
+ /**
87
+ * Puts messages newest first and returns their UIDs in that order.
88
+ *
89
+ * The UID breaks ties, and that is not cosmetic. Two messages can carry the
90
+ * same `Date:` down to the second, and mail sent by a script routinely does.
91
+ * Without a tiebreaker their relative order would be whatever the sort happened
92
+ * to produce that time, which differs between calls, and paging would then be
93
+ * free to show one of them on two pages and the other on none.
94
+ *
95
+ * Messages without a date sort last. They are rare and malformed, but they must
96
+ * land somewhere definite rather than wherever undefined comparisons put them.
97
+ */
98
+ export declare function orderNewestFirst(entries: OrderEntry[]): number[];
99
+ /**
100
+ * Reads the headers for a set of UIDs, keeping the order they were given in.
101
+ *
102
+ * Shared by listing and searching, so both produce identical entries. The
103
+ * caller has already selected the mailbox and narrowed the UIDs down to one
104
+ * page: fetching headers for a whole result set would defeat the purpose, since
105
+ * a common word matched 6803 messages in the measured mailbox.
106
+ */
107
+ export declare function fetchHeaders(client: ImapFlow, uids: number[]): Promise<MessageHeader[]>;
108
+ export interface ListOptions {
109
+ /** How many messages to return. Capped at MAX_LIST_LIMIT. */
110
+ limit?: number;
111
+ /** How many of the newest messages to skip, for paging. */
112
+ offset?: number;
113
+ /** When true, only unread messages are listed. */
114
+ unreadOnly?: boolean;
115
+ }
116
+ /**
117
+ * Lists the headers of a mailbox, newest first.
118
+ *
119
+ * The Bridge supports neither SORT nor THREAD, so ordering happens here, and it
120
+ * has to happen over the whole result set before a page is cut out of it.
121
+ *
122
+ * Ordering by UID instead would be free, since UIDs ascend with arrival. It was
123
+ * how this worked and it was wrong: a message moved into a mailbox arrives now
124
+ * and therefore gets the highest UID, while its `Date:` stays whatever the
125
+ * sender wrote. It would land on the first page with newer messages behind it.
126
+ * Archiving does exactly this, so in an archive the two orders barely agree at
127
+ * all.
128
+ *
129
+ * The price is one extra fetch across every hit, whose cost is handed back in
130
+ * `ordering` rather than hidden.
131
+ */
132
+ export declare function listMessages(connection: Connection, path: string, options?: ListOptions): Promise<ListResult>;
133
+ export interface FullMessage extends ParsedMessage {
134
+ /** The mailbox the message was read from. */
135
+ path: string;
136
+ }
137
+ /**
138
+ * Reads a single message and converts it into readable text.
139
+ *
140
+ * `hint` is the mailbox to look in first, which saves a search. Without it the
141
+ * lookup goes through "All Mail".
142
+ */
143
+ export declare function getMessage(connection: Connection, messageId: string, options?: {
144
+ hint?: string;
145
+ maxTextChars?: number;
146
+ textOffset?: number;
147
+ }): Promise<FullMessage>;
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Listing and reading messages.
3
+ *
4
+ * The guiding constraint is the context window, not the network. A real
5
+ * mailbox has a median message size of roughly 16200 tokens raw, so twenty raw
6
+ * messages exceed a full window. Listings therefore carry headers only, and
7
+ * full text is fetched one message at a time.
8
+ */
9
+ import { BridgeError } from '../bridge/errors.js';
10
+ import { resolveMessageId, presentMessageId } from './ids.js';
11
+ import { parseMessage } from '../mime/parse.js';
12
+ /** Upper bound for one listing. Deliberately low, callers can page. */
13
+ export const MAX_LIST_LIMIT = 100;
14
+ /** Character budget for a single message body. Roughly 6000 tokens. */
15
+ export const DEFAULT_TEXT_BUDGET = 24_000;
16
+ export function toAddresses(list) {
17
+ if (!list)
18
+ return [];
19
+ const out = [];
20
+ for (const entry of list) {
21
+ if (!entry.address)
22
+ continue;
23
+ const item = { address: entry.address };
24
+ if (entry.name)
25
+ item.name = entry.name;
26
+ out.push(item);
27
+ }
28
+ return out;
29
+ }
30
+ /**
31
+ * Guesses from the body structure whether real attachments are present.
32
+ *
33
+ * Cheap, because it works off the structure the server already returns rather
34
+ * than parsing the message. Proton's own public key is excluded, otherwise
35
+ * every sent message would look like it had an attachment.
36
+ */
37
+ export function detectAttachments(node) {
38
+ const n = node;
39
+ if (!n)
40
+ return false;
41
+ if (n.disposition === 'attachment') {
42
+ const filename = n.dispositionParameters?.filename ?? '';
43
+ const isProtonKey = n.type === 'application/pgp-keys' && /^publickey - .+ - 0x[0-9a-f]+\.asc$/i.test(filename);
44
+ if (!isProtonKey)
45
+ return true;
46
+ }
47
+ for (const child of n.childNodes ?? []) {
48
+ if (detectAttachments(child))
49
+ return true;
50
+ }
51
+ return false;
52
+ }
53
+ /**
54
+ * Reads nothing but the date of each message.
55
+ *
56
+ * This is the cheapest fetch IMAP offers for the purpose: ENVELOPE and nothing
57
+ * else. In particular no BODYSTRUCTURE, which is the expensive part of a full
58
+ * header fetch because the server has to walk the MIME tree to produce it.
59
+ *
60
+ * It runs over the entire result set, not over one page, and that is the point:
61
+ * an order can only be established from all of it. What it costs is reported
62
+ * back to the caller rather than swallowed.
63
+ *
64
+ * The date is taken from the envelope, which is the `Date:` field the sender
65
+ * wrote. INTERNALDATE would be cheaper still, but it is the time the server
66
+ * received the message and a copy between mailboxes may set it afresh, which is
67
+ * the very thing this ordering exists to survive.
68
+ */
69
+ export async function fetchDates(client, uids) {
70
+ if (uids.length === 0)
71
+ return [];
72
+ const entries = [];
73
+ for await (const msg of client.fetch(uids, { uid: true, envelope: true }, { uid: true })) {
74
+ entries.push({ uid: msg.uid, date: msg.envelope?.date });
75
+ }
76
+ return entries;
77
+ }
78
+ /**
79
+ * Puts messages newest first and returns their UIDs in that order.
80
+ *
81
+ * The UID breaks ties, and that is not cosmetic. Two messages can carry the
82
+ * same `Date:` down to the second, and mail sent by a script routinely does.
83
+ * Without a tiebreaker their relative order would be whatever the sort happened
84
+ * to produce that time, which differs between calls, and paging would then be
85
+ * free to show one of them on two pages and the other on none.
86
+ *
87
+ * Messages without a date sort last. They are rare and malformed, but they must
88
+ * land somewhere definite rather than wherever undefined comparisons put them.
89
+ */
90
+ export function orderNewestFirst(entries) {
91
+ // An unparsable Date header yields an Invalid Date rather than nothing, and
92
+ // its getTime() is NaN. NaN compares false against everything including
93
+ // itself, so left alone it would scatter such messages wherever the sort
94
+ // happened to walk. Treated as missing, they land at the end like the rest.
95
+ const time = (d) => {
96
+ const t = d?.getTime();
97
+ return t === undefined || Number.isNaN(t) ? undefined : t;
98
+ };
99
+ return [...entries]
100
+ .sort((a, b) => {
101
+ const at = time(a.date);
102
+ const bt = time(b.date);
103
+ if (at !== bt) {
104
+ if (at === undefined)
105
+ return 1;
106
+ if (bt === undefined)
107
+ return -1;
108
+ return bt - at;
109
+ }
110
+ return b.uid - a.uid;
111
+ })
112
+ .map((e) => e.uid);
113
+ }
114
+ /**
115
+ * Reads the headers for a set of UIDs, keeping the order they were given in.
116
+ *
117
+ * Shared by listing and searching, so both produce identical entries. The
118
+ * caller has already selected the mailbox and narrowed the UIDs down to one
119
+ * page: fetching headers for a whole result set would defeat the purpose, since
120
+ * a common word matched 6803 messages in the measured mailbox.
121
+ */
122
+ export async function fetchHeaders(client, uids) {
123
+ if (uids.length === 0)
124
+ return [];
125
+ const byUid = new Map();
126
+ for await (const msg of client.fetch(uids, { uid: true, envelope: true, flags: true, size: true, bodyStructure: true }, { uid: true })) {
127
+ const env = msg.envelope;
128
+ byUid.set(msg.uid, {
129
+ // Through presentMessageId so that a listing names a message the same
130
+ // way reading it does. The envelope repeats the header as it stands.
131
+ messageId: presentMessageId(env?.messageId ?? ''),
132
+ subject: env?.subject ?? '',
133
+ from: toAddresses(env?.from),
134
+ to: toAddresses(env?.to),
135
+ date: env?.date,
136
+ size: msg.size ?? 0,
137
+ seen: msg.flags?.has('\\Seen') ?? false,
138
+ flagged: msg.flags?.has('\\Flagged') ?? false,
139
+ answered: msg.flags?.has('\\Answered') ?? false,
140
+ draft: msg.flags?.has('\\Draft') ?? false,
141
+ hasAttachments: detectAttachments(msg.bodyStructure),
142
+ });
143
+ }
144
+ // fetch does not guarantee the requested order, and the order it was given in
145
+ // is the one the caller established. Sorting by date here instead would throw
146
+ // that away, along with the tiebreaker that makes paging deterministic.
147
+ // A uid that produced no answer is skipped: the message was removed between
148
+ // the search and this fetch.
149
+ return uids.map((uid) => byUid.get(uid)).filter((h) => h !== undefined);
150
+ }
151
+ /**
152
+ * Lists the headers of a mailbox, newest first.
153
+ *
154
+ * The Bridge supports neither SORT nor THREAD, so ordering happens here, and it
155
+ * has to happen over the whole result set before a page is cut out of it.
156
+ *
157
+ * Ordering by UID instead would be free, since UIDs ascend with arrival. It was
158
+ * how this worked and it was wrong: a message moved into a mailbox arrives now
159
+ * and therefore gets the highest UID, while its `Date:` stays whatever the
160
+ * sender wrote. It would land on the first page with newer messages behind it.
161
+ * Archiving does exactly this, so in an archive the two orders barely agree at
162
+ * all.
163
+ *
164
+ * The price is one extra fetch across every hit, whose cost is handed back in
165
+ * `ordering` rather than hidden.
166
+ */
167
+ export async function listMessages(connection, path, options = {}) {
168
+ const limit = Math.min(Math.max(options.limit ?? 25, 1), MAX_LIST_LIMIT);
169
+ const offset = Math.max(options.offset ?? 0, 0);
170
+ const nothing = { messages: 0, elapsedMs: 0 };
171
+ return connection.withMailbox(path, async (client, status) => {
172
+ // The Bridge answers FETCH on an empty mailbox with `BAD no such message`
173
+ // instead of an empty result, so nothing may be fetched here.
174
+ if (status.messages === 0) {
175
+ return { path, total: 0, offset, headers: [], ordering: nothing };
176
+ }
177
+ const uids = await client.search(options.unreadOnly ? { seen: false } : { all: true }, { uid: true });
178
+ if (!uids || uids.length === 0) {
179
+ return { path, total: 0, offset, headers: [], ordering: nothing };
180
+ }
181
+ const started = process.hrtime.bigint();
182
+ const ordered = orderNewestFirst(await fetchDates(client, uids));
183
+ const ordering = {
184
+ messages: uids.length,
185
+ elapsedMs: Math.round(Number(process.hrtime.bigint() - started) / 1e6),
186
+ };
187
+ const page = ordered.slice(offset, offset + limit);
188
+ if (page.length === 0) {
189
+ return { path, total: ordered.length, offset, headers: [], ordering };
190
+ }
191
+ const headers = await fetchHeaders(client, page);
192
+ return { path, total: ordered.length, offset, headers, ordering };
193
+ });
194
+ }
195
+ /**
196
+ * Reads a single message and converts it into readable text.
197
+ *
198
+ * `hint` is the mailbox to look in first, which saves a search. Without it the
199
+ * lookup goes through "All Mail".
200
+ */
201
+ export async function getMessage(connection, messageId, options = {}) {
202
+ const resolved = await resolveMessageId(connection, messageId, options.hint);
203
+ const source = await connection.withMailbox(resolved.path, async (client) => {
204
+ const message = await client.fetchOne(String(resolved.uid), { source: true }, { uid: true });
205
+ if (!message || !message.source)
206
+ return undefined;
207
+ return message.source;
208
+ });
209
+ if (!source) {
210
+ throw new BridgeError(`The message ${resolved.messageId} was found in "${resolved.path}" but could not be read. ` +
211
+ 'It may have been moved or deleted in the meantime. Try again.');
212
+ }
213
+ const parsed = await parseMessage(source, {
214
+ maxTextChars: options.maxTextChars ?? DEFAULT_TEXT_BUDGET,
215
+ textOffset: options.textOffset ?? 0,
216
+ });
217
+ return { ...parsed, path: resolved.path };
218
+ }
219
+ //# sourceMappingURL=messages.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"messages.js","sourceRoot":"","sources":["../../src/mail/messages.ts"],"names":[],"mappings":"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"}