@rndmjoker/proton-mcp 0.4.11 → 0.4.13
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/README.md +61 -16
- package/dist/bridge/certificate.d.ts +33 -1
- package/dist/bridge/certificate.js +82 -9
- package/dist/bridge/certificate.js.map +1 -1
- package/dist/bridge/connection.js +31 -6
- package/dist/bridge/connection.js.map +1 -1
- package/dist/bridge/errors.d.ts +8 -0
- package/dist/bridge/errors.js +28 -3
- package/dist/bridge/errors.js.map +1 -1
- package/dist/config.d.ts +6 -0
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/credentials/availability.d.ts +5 -0
- package/dist/credentials/availability.js +7 -0
- package/dist/credentials/availability.js.map +1 -1
- package/dist/credentials/encrypted-file.js +7 -2
- package/dist/credentials/encrypted-file.js.map +1 -1
- package/dist/credentials/keyring.d.ts +12 -2
- package/dist/credentials/keyring.js +44 -6
- package/dist/credentials/keyring.js.map +1 -1
- package/dist/credentials/plain-file.js +8 -2
- package/dist/credentials/plain-file.js.map +1 -1
- package/dist/credentials/session-bus.d.ts +38 -0
- package/dist/credentials/session-bus.js +68 -0
- package/dist/credentials/session-bus.js.map +1 -0
- package/dist/credentials/store.d.ts +5 -1
- package/dist/credentials/store.js.map +1 -1
- package/dist/mail/attachments.d.ts +12 -2
- package/dist/mail/attachments.js +19 -11
- package/dist/mail/attachments.js.map +1 -1
- package/dist/mail/compose.d.ts +65 -9
- package/dist/mail/compose.js +147 -11
- package/dist/mail/compose.js.map +1 -1
- package/dist/mail/drafts.d.ts +13 -2
- package/dist/mail/drafts.js +69 -18
- package/dist/mail/drafts.js.map +1 -1
- package/dist/mail/ids.js +6 -4
- package/dist/mail/ids.js.map +1 -1
- package/dist/mail/messages.d.ts +25 -2
- package/dist/mail/messages.js +37 -4
- package/dist/mail/messages.js.map +1 -1
- package/dist/mail/saved-attachments.d.ts +128 -0
- package/dist/mail/saved-attachments.js +448 -0
- package/dist/mail/saved-attachments.js.map +1 -0
- package/dist/mail/search.d.ts +67 -3
- package/dist/mail/search.js +134 -23
- package/dist/mail/search.js.map +1 -1
- package/dist/mail/send.js +24 -9
- package/dist/mail/send.js.map +1 -1
- package/dist/mail/uid-search.d.ts +17 -0
- package/dist/mail/uid-search.js +25 -0
- package/dist/mail/uid-search.js.map +1 -0
- package/dist/mime/filenames.d.ts +24 -0
- package/dist/mime/filenames.js +58 -0
- package/dist/mime/filenames.js.map +1 -0
- package/dist/mime/parts.d.ts +34 -0
- package/dist/mime/parts.js +92 -0
- package/dist/mime/parts.js.map +1 -0
- package/dist/server.js +39 -6
- package/dist/server.js.map +1 -1
- package/dist/session.d.ts +15 -3
- package/dist/session.js +35 -8
- package/dist/session.js.map +1 -1
- package/dist/tools/confirm.js +39 -11
- package/dist/tools/confirm.js.map +1 -1
- package/dist/tools/drafts.d.ts +3 -0
- package/dist/tools/drafts.js +18 -6
- package/dist/tools/drafts.js.map +1 -1
- package/dist/tools/filters.d.ts +40 -0
- package/dist/tools/filters.js +89 -0
- package/dist/tools/filters.js.map +1 -0
- package/dist/tools/format.d.ts +8 -0
- package/dist/tools/format.js +29 -6
- package/dist/tools/format.js.map +1 -1
- package/dist/tools/guidance.js +27 -3
- package/dist/tools/guidance.js.map +1 -1
- package/dist/tools/messages.js +7 -2
- package/dist/tools/messages.js.map +1 -1
- package/dist/tools/pending-view.d.ts +7 -0
- package/dist/tools/pending-view.js +4 -1
- package/dist/tools/pending-view.js.map +1 -1
- package/dist/tools/saved.d.ts +15 -0
- package/dist/tools/saved.js +107 -0
- package/dist/tools/saved.js.map +1 -0
- package/dist/tools/send.js +4 -1
- package/dist/tools/send.js.map +1 -1
- package/dist/web/layout.js +2 -0
- package/dist/web/layout.js.map +1 -1
- package/dist/web/pages.js +1 -0
- package/dist/web/pages.js.map +1 -1
- package/dist/web/pending.d.ts +4 -0
- package/dist/web/pending.js +32 -4
- package/dist/web/pending.js.map +1 -1
- package/dist/web/server.d.ts +16 -5
- package/dist/web/server.js +12 -8
- package/dist/web/server.js.map +1 -1
- package/package.json +16 -8
package/dist/mail/search.js
CHANGED
|
@@ -12,24 +12,115 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Metadata queries are cheap by comparison: unread came back in 3 ms, a size
|
|
14
14
|
* filter in 437 ms.
|
|
15
|
+
*
|
|
16
|
+
* Text is the exception to "the Bridge searches". Measured on 2026-11-02 and
|
|
17
|
+
* read in the source of gluon, its IMAP library: SUBJECT, FROM and TO compare
|
|
18
|
+
* against the header as it is stored, and a header with one letter outside
|
|
19
|
+
* ASCII is stored encoded, "Mit Anhang" as "Mit_Anhang", or in base64, where no
|
|
20
|
+
* word is legible at all. Up to 0.4.11 a subject search for "Mit Anhang" found
|
|
21
|
+
* nothing, and the answer read like a search without matches. So the Bridge
|
|
22
|
+
* now filters by everything but those three, and this server compares them
|
|
23
|
+
* against the envelope, which imapflow decodes. Full text stays with the
|
|
24
|
+
* Bridge, which compares the raw bytes; a word outside ASCII is also asked for
|
|
25
|
+
* in the form a quoted-printable part carries it, and a part sent as base64
|
|
26
|
+
* stays out of reach, which every answer says.
|
|
15
27
|
*/
|
|
16
28
|
import { BridgeError } from '../bridge/errors.js';
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
29
|
+
import { searchUids } from './uid-search.js';
|
|
30
|
+
import { fetchEnvelopes, fetchHeaders, orderNewestFirst, MAX_LIST_LIMIT, withTrashMarks, } from './messages.js';
|
|
31
|
+
/**
|
|
32
|
+
* How many envelopes a search compares itself, at most.
|
|
33
|
+
*
|
|
34
|
+
* Without other criteria a subject search reads the envelope of every message
|
|
35
|
+
* in the mailbox. Reading one per hit is what the ordering has always cost, and
|
|
36
|
+
* a common word matched 6803 messages in a mailbox of 26816, but there has to
|
|
37
|
+
* be an end. Past it, the most recently added messages are compared and the
|
|
38
|
+
* answer says so; there is no silent cut.
|
|
39
|
+
*/
|
|
40
|
+
export const MAX_COMPARED = 20_000;
|
|
41
|
+
/** Whether a byte sequence has anything outside ASCII. */
|
|
42
|
+
const outsideAscii = (text) => /[^\x00-\x7f]/.test(text);
|
|
43
|
+
/**
|
|
44
|
+
* A word as a quoted-printable part carries it: every byte of its UTF-8 form
|
|
45
|
+
* outside printable ASCII, and the equals sign, as =XX.
|
|
46
|
+
*/
|
|
47
|
+
export function quotedPrintable(text) {
|
|
48
|
+
let out = '';
|
|
49
|
+
for (const byte of Buffer.from(text.normalize('NFC'), 'utf8')) {
|
|
50
|
+
out +=
|
|
51
|
+
byte > 0x7e || byte < 0x20 || byte === 0x3d
|
|
52
|
+
? `=${byte.toString(16).toUpperCase().padStart(2, '0')}`
|
|
53
|
+
: String.fromCharCode(byte);
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The forms a full-text word is asked for in.
|
|
59
|
+
*
|
|
60
|
+
* The Bridge compares the raw bytes of a message, lower cased, so a word with a
|
|
61
|
+
* letter outside ASCII is found only where a part carries it unencoded. Most
|
|
62
|
+
* text with umlauts travels as quoted-printable, where a u with umlaut is the
|
|
63
|
+
* six characters "=C3=BC" and an a with umlaut "=C3=A4". Lower casing does not reach into
|
|
64
|
+
* that encoding, so the usual spellings of a word are each asked for: as given,
|
|
65
|
+
* in lower case, and capitalised. Forms that differ only in the case of ASCII
|
|
66
|
+
* letters are asked once, since the Bridge lower cases both sides. A word
|
|
67
|
+
* broken across a line by the encoding is still missed, and so is anything in
|
|
68
|
+
* base64.
|
|
69
|
+
*/
|
|
70
|
+
export function textForms(term) {
|
|
71
|
+
if (!outsideAscii(term))
|
|
72
|
+
return [term];
|
|
73
|
+
const lower = term.toLowerCase();
|
|
74
|
+
const capitalised = lower.charAt(0).toUpperCase() + lower.slice(1);
|
|
75
|
+
const forms = new Map([[term.toLowerCase(), term]]);
|
|
76
|
+
for (const spelling of [term, lower, capitalised]) {
|
|
77
|
+
const form = quotedPrintable(spelling);
|
|
78
|
+
if (!forms.has(form.toLowerCase()))
|
|
79
|
+
forms.set(form.toLowerCase(), form);
|
|
80
|
+
}
|
|
81
|
+
return [...forms.values()];
|
|
82
|
+
}
|
|
83
|
+
/** Folded for comparison: one Unicode form, lower case. */
|
|
84
|
+
const fold = (text) => text.normalize('NFC').toLowerCase();
|
|
85
|
+
/** How an address reads in a header, which is what FROM and TO used to match. */
|
|
86
|
+
const shown = (a) => (a.name ? `${a.name} <${a.address}>` : a.address);
|
|
87
|
+
/** Whether a message passes what this server compares itself. */
|
|
88
|
+
export function matchesLocally(entry, local) {
|
|
89
|
+
if (local.subject !== undefined && !fold(entry.subject).includes(fold(local.subject)))
|
|
90
|
+
return false;
|
|
91
|
+
if (local.from !== undefined && !entry.from.some((a) => fold(shown(a)).includes(fold(local.from ?? '')))) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
if (local.to !== undefined && !entry.to.some((a) => fold(shown(a)).includes(fold(local.to ?? '')))) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Splits the criteria into what the Bridge is asked and what this server
|
|
101
|
+
* compares itself.
|
|
102
|
+
*/
|
|
103
|
+
export function planSearch(criteria) {
|
|
20
104
|
// Typed as what it is rather than built loosely and cast at the end. The cast
|
|
21
105
|
// was how `since` survived: a field name imapflow does not know is silently
|
|
22
106
|
// an ignored criterion, so the search returns too much and nothing complains.
|
|
23
107
|
// With the real type a wrong name is a compile error.
|
|
24
108
|
const query = {};
|
|
25
|
-
|
|
26
|
-
|
|
109
|
+
const local = {};
|
|
110
|
+
if (criteria.text) {
|
|
111
|
+
const forms = textForms(criteria.text);
|
|
112
|
+
if (forms.length === 1)
|
|
113
|
+
query.text = criteria.text;
|
|
114
|
+
else
|
|
115
|
+
query.or = forms.map((form) => ({ text: form }));
|
|
116
|
+
}
|
|
117
|
+
// Compared here rather than by the Bridge, see the top of this file.
|
|
27
118
|
if (criteria.subject)
|
|
28
|
-
|
|
119
|
+
local.subject = criteria.subject;
|
|
29
120
|
if (criteria.from)
|
|
30
|
-
|
|
121
|
+
local.from = criteria.from;
|
|
31
122
|
if (criteria.to)
|
|
32
|
-
|
|
123
|
+
local.to = criteria.to;
|
|
33
124
|
// sentSince and sentBefore, not since and before. Those two are IMAP SINCE
|
|
34
125
|
// and BEFORE, which compare INTERNALDATE, the moment the server received the
|
|
35
126
|
// message. Everything this server shows and sorts by comes from the envelope,
|
|
@@ -50,11 +141,16 @@ export function buildQuery(criteria) {
|
|
|
50
141
|
query.larger = criteria.largerThan;
|
|
51
142
|
// An empty query would match everything, which is list_messages rather than
|
|
52
143
|
// a search. Saying so is better than silently returning the whole mailbox.
|
|
53
|
-
|
|
144
|
+
const comparedHere = Object.keys(local).length > 0;
|
|
145
|
+
if (Object.keys(query).length === 0 && !comparedHere) {
|
|
54
146
|
throw new BridgeError('A search needs at least one criterion. Pass text, subject, from, to, since, before, ' +
|
|
55
147
|
'unread, starred or largerThan. To see everything in a mailbox use list_messages instead.');
|
|
56
148
|
}
|
|
57
|
-
|
|
149
|
+
// Only the comparison here narrows it down, so the Bridge hands over every
|
|
150
|
+
// message and this server reads their envelopes.
|
|
151
|
+
if (Object.keys(query).length === 0)
|
|
152
|
+
query.all = true;
|
|
153
|
+
return { query, local };
|
|
58
154
|
}
|
|
59
155
|
/** Whether a query contains the expensive full-text part. */
|
|
60
156
|
export function isFullText(criteria) {
|
|
@@ -71,15 +167,15 @@ export async function searchMessages(connection, path, options) {
|
|
|
71
167
|
const limit = Math.min(Math.max(rawLimit ?? 25, 1), MAX_LIST_LIMIT);
|
|
72
168
|
const offset = Math.max(rawOffset ?? 0, 0);
|
|
73
169
|
// Built before opening the mailbox, so an empty query fails fast.
|
|
74
|
-
const query =
|
|
170
|
+
const { query, local } = planSearch(criteria);
|
|
171
|
+
const comparedHere = Object.keys(local).length > 0;
|
|
172
|
+
const fullText = isFullText(criteria);
|
|
173
|
+
const encodedForms = Boolean(criteria.text) && textForms(criteria.text ?? '').length > 1;
|
|
75
174
|
const started = process.hrtime.bigint();
|
|
76
175
|
const uids = await connection.withMailbox(path, async (client, status) => {
|
|
77
176
|
if (status.messages === 0)
|
|
78
177
|
return [];
|
|
79
|
-
|
|
80
|
-
// imapflow answers with false rather than an empty array when a search
|
|
81
|
-
// yields nothing, and `?? []` would let that through.
|
|
82
|
-
return Array.isArray(hits) ? hits : [];
|
|
178
|
+
return searchUids(client, query, `messages in "${path}"`);
|
|
83
179
|
});
|
|
84
180
|
const elapsedMs = Math.round(Number(process.hrtime.bigint() - started) / 1e6);
|
|
85
181
|
if (uids.length === 0) {
|
|
@@ -90,17 +186,30 @@ export async function searchMessages(connection, path, options) {
|
|
|
90
186
|
headers: [],
|
|
91
187
|
elapsedMs,
|
|
92
188
|
ordering: { messages: 0, elapsedMs: 0 },
|
|
93
|
-
fullText
|
|
189
|
+
fullText,
|
|
190
|
+
encodedForms,
|
|
94
191
|
};
|
|
95
192
|
}
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
193
|
+
// Past the limit the most recently added are compared: the highest uids. The
|
|
194
|
+
// answer says so, see MAX_COMPARED.
|
|
195
|
+
let candidates = uids;
|
|
196
|
+
let compared;
|
|
197
|
+
if (comparedHere && uids.length > MAX_COMPARED) {
|
|
198
|
+
candidates = [...uids].sort((a, b) => b - a).slice(0, MAX_COMPARED);
|
|
199
|
+
compared = { compared: MAX_COMPARED, of: uids.length };
|
|
200
|
+
}
|
|
201
|
+
// Ordering reads the envelope of every candidate, because a page can only be
|
|
202
|
+
// cut out of a list that is already in order. The same read carries the
|
|
203
|
+
// subject, sender and recipients compared here. Only the envelopes: the full
|
|
204
|
+
// header fetch below still covers one page, which is what keeps this
|
|
205
|
+
// affordable. A common word matched 6803 messages in the measured mailbox.
|
|
100
206
|
const orderStarted = process.hrtime.bigint();
|
|
101
|
-
const ordered = await connection.withMailbox(path, async (client) =>
|
|
207
|
+
const ordered = await connection.withMailbox(path, async (client) => {
|
|
208
|
+
const entries = await fetchEnvelopes(client, candidates);
|
|
209
|
+
return orderNewestFirst(comparedHere ? entries.filter((entry) => matchesLocally(entry, local)) : entries);
|
|
210
|
+
});
|
|
102
211
|
const ordering = {
|
|
103
|
-
messages:
|
|
212
|
+
messages: candidates.length,
|
|
104
213
|
elapsedMs: Math.round(Number(process.hrtime.bigint() - orderStarted) / 1e6),
|
|
105
214
|
};
|
|
106
215
|
const page = ordered.slice(offset, offset + limit);
|
|
@@ -113,7 +222,9 @@ export async function searchMessages(connection, path, options) {
|
|
|
113
222
|
headers,
|
|
114
223
|
elapsedMs,
|
|
115
224
|
ordering,
|
|
116
|
-
fullText
|
|
225
|
+
fullText,
|
|
226
|
+
encodedForms,
|
|
227
|
+
...(compared ? { compared } : {}),
|
|
117
228
|
});
|
|
118
229
|
}
|
|
119
230
|
//# sourceMappingURL=search.js.map
|
package/dist/mail/search.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search.js","sourceRoot":"","sources":["../../src/mail/search.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"search.js","sourceRoot":"","sources":["../../src/mail/search.ts"],"names":[],"mappings":"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"}
|
package/dist/mail/send.js
CHANGED
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
* envelope is stated separately below.
|
|
41
41
|
*/
|
|
42
42
|
import { createTransport } from 'nodemailer';
|
|
43
|
-
import { BridgeError } from '../bridge/errors.js';
|
|
44
|
-
import {
|
|
43
|
+
import { BridgeError, explainError, isConnectionFailure } from '../bridge/errors.js';
|
|
44
|
+
import { certificateChanged, pinFor } from '../bridge/certificate.js';
|
|
45
45
|
import { buildMessage, allRecipients } from './compose.js';
|
|
46
46
|
/**
|
|
47
47
|
* Opens an authenticated SMTP connection to the Bridge.
|
|
@@ -49,12 +49,7 @@ import { buildMessage, allRecipients } from './compose.js';
|
|
|
49
49
|
* Built per send rather than held open. Sending is rare, and a connection that
|
|
50
50
|
* sits idle holding the Bridge password is a worse trade than a handshake.
|
|
51
51
|
*/
|
|
52
|
-
|
|
53
|
-
// The pin is recorded against the IMAP port because that is where the first
|
|
54
|
-
// connection happens. Measured to be the same certificate on the SMTP port,
|
|
55
|
-
// so this is the material to verify against; a difference fails the
|
|
56
|
-
// handshake rather than being waved through.
|
|
57
|
-
const pinned = await ensurePinned(config.host, config.imapPort);
|
|
52
|
+
function transport(config, credentials, pinned) {
|
|
58
53
|
return createTransport({
|
|
59
54
|
host: config.host,
|
|
60
55
|
port: config.smtpPort,
|
|
@@ -104,7 +99,17 @@ export async function sendMessage(config, getCredentials, readOnly, draft) {
|
|
|
104
99
|
// Built without the blind copies in the headers. They travel in the envelope
|
|
105
100
|
// below, where the other recipients never see them.
|
|
106
101
|
const raw = await buildMessage(draft, { keepBcc: false });
|
|
107
|
-
|
|
102
|
+
// The pin is recorded against the IMAP port because that is where the first
|
|
103
|
+
// connection happens. Measured to be the same certificate on the SMTP port,
|
|
104
|
+
// so this is the material to verify against; a difference fails the
|
|
105
|
+
// handshake rather than being waved through.
|
|
106
|
+
//
|
|
107
|
+
// On first use reading it is the first thing that reaches the Bridge, so a
|
|
108
|
+
// Bridge that is not running fails here, before the try below.
|
|
109
|
+
const pinned = await pinFor(config.host, config.imapPort).catch((error) => {
|
|
110
|
+
throw error instanceof BridgeError ? error : explainError(error, config.host, config.imapPort);
|
|
111
|
+
});
|
|
112
|
+
const mailer = transport(config, credentials, pinned);
|
|
108
113
|
try {
|
|
109
114
|
const info = await mailer.sendMail({
|
|
110
115
|
// The bytes, not the fields: what was confirmed is what goes out.
|
|
@@ -122,6 +127,16 @@ export async function sendMessage(config, getCredentials, readOnly, draft) {
|
|
|
122
127
|
};
|
|
123
128
|
}
|
|
124
129
|
catch (error) {
|
|
130
|
+
// A certificate other than the pinned one fails the handshake before AUTH,
|
|
131
|
+
// and from here that looks like any other failure. Only now is a probe
|
|
132
|
+
// worth it.
|
|
133
|
+
const changed = await certificateChanged(config.host, config.imapPort, pinned);
|
|
134
|
+
if (changed)
|
|
135
|
+
throw changed;
|
|
136
|
+
// With a pin there is no probe in front of the send any more, so a Bridge
|
|
137
|
+
// that is not running is first noticed here, and is explained the same way.
|
|
138
|
+
if (isConnectionFailure(error))
|
|
139
|
+
throw explainError(error, config.host, config.smtpPort);
|
|
125
140
|
throw new BridgeError(`The Bridge did not accept the message: ${error.message}. ` +
|
|
126
141
|
'Nothing was sent, and it can be sent again once the Bridge is reachable.', 'unreachable', { cause: error });
|
|
127
142
|
}
|
package/dist/mail/send.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"send.js","sourceRoot":"","sources":["../../src/mail/send.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,OAAO,EAAE,eAAe,EAAoB,MAAM,YAAY,CAAA;AAE9D,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;
|
|
1
|
+
{"version":3,"file":"send.js","sourceRoot":"","sources":["../../src/mail/send.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A UID SEARCH that tells a failure apart from an empty result.
|
|
3
|
+
*
|
|
4
|
+
* imapflow answers a search that matched nothing with an empty array, and a
|
|
5
|
+
* search that failed with `false`: it catches the error, logs it and returns.
|
|
6
|
+
* The logger is switched off here, so `false` is all that arrives. Read as
|
|
7
|
+
* "nothing matched", a failed search makes a mailbox look empty and a message
|
|
8
|
+
* look as though it did not exist, and the assistant goes on to act on an
|
|
9
|
+
* answer that never came.
|
|
10
|
+
*/
|
|
11
|
+
import type { ImapFlow, SearchObject } from 'imapflow';
|
|
12
|
+
/**
|
|
13
|
+
* Runs the search and returns the matching UIDs, or throws when it failed.
|
|
14
|
+
*
|
|
15
|
+
* `what` names what was being looked for, so the refusal can say it.
|
|
16
|
+
*/
|
|
17
|
+
export declare function searchUids(client: ImapFlow, query: SearchObject, what: string): Promise<number[]>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A UID SEARCH that tells a failure apart from an empty result.
|
|
3
|
+
*
|
|
4
|
+
* imapflow answers a search that matched nothing with an empty array, and a
|
|
5
|
+
* search that failed with `false`: it catches the error, logs it and returns.
|
|
6
|
+
* The logger is switched off here, so `false` is all that arrives. Read as
|
|
7
|
+
* "nothing matched", a failed search makes a mailbox look empty and a message
|
|
8
|
+
* look as though it did not exist, and the assistant goes on to act on an
|
|
9
|
+
* answer that never came.
|
|
10
|
+
*/
|
|
11
|
+
import { BridgeError } from '../bridge/errors.js';
|
|
12
|
+
/**
|
|
13
|
+
* Runs the search and returns the matching UIDs, or throws when it failed.
|
|
14
|
+
*
|
|
15
|
+
* `what` names what was being looked for, so the refusal can say it.
|
|
16
|
+
*/
|
|
17
|
+
export async function searchUids(client, query, what) {
|
|
18
|
+
const hits = await client.search(query, { uid: true });
|
|
19
|
+
if (Array.isArray(hits))
|
|
20
|
+
return hits;
|
|
21
|
+
throw new BridgeError(`The search for ${what} failed, so this says nothing about which messages match: it is not ` +
|
|
22
|
+
'an empty result. The Bridge gave no reason. Try again, and if it fails again, check that ' +
|
|
23
|
+
'the Bridge is running and unlocked.');
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=uid-search.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"uid-search.js","sourceRoot":"","sources":["../../src/mail/uid-search.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AAEjD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,MAAgB,EAChB,KAAmB,EACnB,IAAY;IAEZ,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAA;IACtD,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAA;IACpC,MAAM,IAAI,WAAW,CACnB,kBAAkB,IAAI,sEAAsE;QAC1F,2FAA2F;QAC3F,qCAAqC,CACxC,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File names taken from a message, made safe to hand on.
|
|
3
|
+
*
|
|
4
|
+
* A name in a message was chosen by whoever wrote it. It ends up as the name of
|
|
5
|
+
* an attachment this server builds, and with save_attachment as a name on this
|
|
6
|
+
* machine's disk, so the same rule serves both.
|
|
7
|
+
*
|
|
8
|
+
* Letters and digits of every script stay. An attachment name may carry them
|
|
9
|
+
* (RFC 2231), and a name with underscores where its umlauts were helps nobody:
|
|
10
|
+
* up to 0.4.11 a forwarded original named after a German subject lost every
|
|
11
|
+
* one of them. Everything else becomes an underscore: path separators, the
|
|
12
|
+
* characters Windows forbids in a name, control characters, and formatting
|
|
13
|
+
* characters such as U+202E, which can make "txt.exe" read as "exe.txt".
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Reduces a name to a plain file name.
|
|
17
|
+
*
|
|
18
|
+
* Never empty and never a path: the result has no separator, does not start
|
|
19
|
+
* with a dot and is not a device name on Windows. `fallback` is used when
|
|
20
|
+
* nothing usable is left, and has to be plain itself.
|
|
21
|
+
*/
|
|
22
|
+
export declare function plainFileName(name: string, fallback: string, maxLength?: number): string;
|
|
23
|
+
/** The name a forwarded original travels under, built from its subject. */
|
|
24
|
+
export declare function forwardedFileName(subject: string | undefined): string;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File names taken from a message, made safe to hand on.
|
|
3
|
+
*
|
|
4
|
+
* A name in a message was chosen by whoever wrote it. It ends up as the name of
|
|
5
|
+
* an attachment this server builds, and with save_attachment as a name on this
|
|
6
|
+
* machine's disk, so the same rule serves both.
|
|
7
|
+
*
|
|
8
|
+
* Letters and digits of every script stay. An attachment name may carry them
|
|
9
|
+
* (RFC 2231), and a name with underscores where its umlauts were helps nobody:
|
|
10
|
+
* up to 0.4.11 a forwarded original named after a German subject lost every
|
|
11
|
+
* one of them. Everything else becomes an underscore: path separators, the
|
|
12
|
+
* characters Windows forbids in a name, control characters, and formatting
|
|
13
|
+
* characters such as U+202E, which can make "txt.exe" read as "exe.txt".
|
|
14
|
+
*/
|
|
15
|
+
/** Everything that is not a letter, a mark, a digit or one of a few separators. */
|
|
16
|
+
const OUTSIDE_PLAIN = /[^\p{L}\p{M}\p{N} ._()-]/gu;
|
|
17
|
+
/** Names Windows reserves for devices, with or without an extension. */
|
|
18
|
+
const RESERVED = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\..*)?$/i;
|
|
19
|
+
/** An extension worth keeping when a name has to be shortened. */
|
|
20
|
+
const EXTENSION = /\.[\p{L}\p{N}]{1,10}$/u;
|
|
21
|
+
/** Drops the dots and spaces Windows strips without asking, and hidden-file dots. */
|
|
22
|
+
const trimEdges = (name) => name.replace(/^[. ]+/, '').replace(/[. ]+$/, '');
|
|
23
|
+
/**
|
|
24
|
+
* Shortens to a number of characters, not UTF-16 units, so a letter outside the
|
|
25
|
+
* basic plane is never cut in half. The extension survives when there is one.
|
|
26
|
+
*/
|
|
27
|
+
function shorten(name, maxLength) {
|
|
28
|
+
const chars = Array.from(name);
|
|
29
|
+
if (chars.length <= maxLength)
|
|
30
|
+
return name;
|
|
31
|
+
const extension = EXTENSION.exec(name)?.[0] ?? '';
|
|
32
|
+
const extensionChars = Array.from(extension);
|
|
33
|
+
const base = chars.slice(0, chars.length - extensionChars.length);
|
|
34
|
+
const room = maxLength - extensionChars.length;
|
|
35
|
+
// Cutting can leave a dot or a space at the new end, which trimEdges would
|
|
36
|
+
// have removed had it been there before.
|
|
37
|
+
return trimEdges(base.slice(0, Math.max(room, 1)).join('')) + extension;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Reduces a name to a plain file name.
|
|
41
|
+
*
|
|
42
|
+
* Never empty and never a path: the result has no separator, does not start
|
|
43
|
+
* with a dot and is not a device name on Windows. `fallback` is used when
|
|
44
|
+
* nothing usable is left, and has to be plain itself.
|
|
45
|
+
*/
|
|
46
|
+
export function plainFileName(name, fallback, maxLength = 100) {
|
|
47
|
+
let result = trimEdges(name.normalize('NFC').replace(OUTSIDE_PLAIN, '_'));
|
|
48
|
+
if (!result)
|
|
49
|
+
result = fallback;
|
|
50
|
+
if (RESERVED.test(result))
|
|
51
|
+
result = `_${result}`;
|
|
52
|
+
return shorten(result, maxLength);
|
|
53
|
+
}
|
|
54
|
+
/** The name a forwarded original travels under, built from its subject. */
|
|
55
|
+
export function forwardedFileName(subject) {
|
|
56
|
+
return `${plainFileName(subject ?? '', 'message', 60)}.eml`;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=filenames.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filenames.js","sourceRoot":"","sources":["../../src/mime/filenames.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,mFAAmF;AACnF,MAAM,aAAa,GAAG,4BAA4B,CAAA;AAElD,wEAAwE;AACxE,MAAM,QAAQ,GAAG,+CAA+C,CAAA;AAEhE,kEAAkE;AAClE,MAAM,SAAS,GAAG,wBAAwB,CAAA;AAE1C,qFAAqF;AACrF,MAAM,SAAS,GAAG,CAAC,IAAY,EAAU,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAA;AAE5F;;;GAGG;AACH,SAAS,OAAO,CAAC,IAAY,EAAE,SAAiB;IAC9C,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAC9B,IAAI,KAAK,CAAC,MAAM,IAAI,SAAS;QAAE,OAAO,IAAI,CAAA;IAE1C,MAAM,SAAS,GAAG,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;IACjD,MAAM,cAAc,GAAG,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;IAC5C,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,cAAc,CAAC,MAAM,CAAC,CAAA;IACjE,MAAM,IAAI,GAAG,SAAS,GAAG,cAAc,CAAC,MAAM,CAAA;IAC9C,2EAA2E;IAC3E,yCAAyC;IACzC,OAAO,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,GAAG,SAAS,CAAA;AACzE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,IAAY,EAAE,QAAgB,EAAE,SAAS,GAAG,GAAG;IAC3E,IAAI,MAAM,GAAG,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC,CAAA;IACzE,IAAI,CAAC,MAAM;QAAE,MAAM,GAAG,QAAQ,CAAA;IAC9B,IAAI,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC;QAAE,MAAM,GAAG,IAAI,MAAM,EAAE,CAAA;IAChD,OAAO,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;AACnC,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,iBAAiB,CAAC,OAA2B;IAC3D,OAAO,GAAG,aAAa,CAAC,OAAO,IAAI,EAAE,EAAE,SAAS,EAAE,EAAE,CAAC,MAAM,CAAA;AAC7D,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The parts of a stored message, read so that they can be built into a message
|
|
3
|
+
* again unchanged.
|
|
4
|
+
*
|
|
5
|
+
* mailparser is the wrong tool for this, measured with mailparser 3.14.1. It
|
|
6
|
+
* descends into a forwarded original (message/rfc822) and writes it into the
|
|
7
|
+
* body as a table of its headers, so the original disappears from the
|
|
8
|
+
* attachments and turns up as markup nobody wrote. And it rewrites every cid:
|
|
9
|
+
* image into a data: address, which the markup check refuses at every level. A
|
|
10
|
+
* stored draft read back that way could not be sent as it was stored.
|
|
11
|
+
*
|
|
12
|
+
* mailsplit, the splitter mailparser itself is built on, keeps a forwarded
|
|
13
|
+
* original as one part when asked to and hands out the bytes of every part.
|
|
14
|
+
* The body here is the first text/plain and the first text/html part that is
|
|
15
|
+
* not an attachment; everything else is a file, carried along as it was.
|
|
16
|
+
*/
|
|
17
|
+
/** A file inside a stored message, with its content. */
|
|
18
|
+
export interface CarriedFile {
|
|
19
|
+
filename: string;
|
|
20
|
+
contentType: string;
|
|
21
|
+
content: Buffer;
|
|
22
|
+
/** Set when the markup refers to the file by cid. */
|
|
23
|
+
contentId?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface StoredParts {
|
|
26
|
+
/** The plain text body, when there is one. */
|
|
27
|
+
text?: string;
|
|
28
|
+
/** The markup body, with its cid: references as they were. */
|
|
29
|
+
html?: string;
|
|
30
|
+
/** Every other part, in the order the message carries them. */
|
|
31
|
+
files: CarriedFile[];
|
|
32
|
+
}
|
|
33
|
+
/** Reads a stored message into its body and the files it carries. */
|
|
34
|
+
export declare function readParts(source: Buffer): Promise<StoredParts>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The parts of a stored message, read so that they can be built into a message
|
|
3
|
+
* again unchanged.
|
|
4
|
+
*
|
|
5
|
+
* mailparser is the wrong tool for this, measured with mailparser 3.14.1. It
|
|
6
|
+
* descends into a forwarded original (message/rfc822) and writes it into the
|
|
7
|
+
* body as a table of its headers, so the original disappears from the
|
|
8
|
+
* attachments and turns up as markup nobody wrote. And it rewrites every cid:
|
|
9
|
+
* image into a data: address, which the markup check refuses at every level. A
|
|
10
|
+
* stored draft read back that way could not be sent as it was stored.
|
|
11
|
+
*
|
|
12
|
+
* mailsplit, the splitter mailparser itself is built on, keeps a forwarded
|
|
13
|
+
* original as one part when asked to and hands out the bytes of every part.
|
|
14
|
+
* The body here is the first text/plain and the first text/html part that is
|
|
15
|
+
* not an attachment; everything else is a file, carried along as it was.
|
|
16
|
+
*/
|
|
17
|
+
import { Splitter } from '@zone-eu/mailsplit';
|
|
18
|
+
import libmime from 'libmime';
|
|
19
|
+
import { isProtonPublicKey } from './parse.js';
|
|
20
|
+
/** Runs bytes through a stream and collects what comes out. */
|
|
21
|
+
async function through(stream, chunks) {
|
|
22
|
+
const out = [];
|
|
23
|
+
const done = (async () => {
|
|
24
|
+
for await (const chunk of stream)
|
|
25
|
+
out.push(Buffer.from(chunk));
|
|
26
|
+
})();
|
|
27
|
+
for (const chunk of chunks)
|
|
28
|
+
stream.write(chunk);
|
|
29
|
+
stream.end();
|
|
30
|
+
await done;
|
|
31
|
+
return Buffer.concat(out);
|
|
32
|
+
}
|
|
33
|
+
async function asText(node, decoded) {
|
|
34
|
+
const text = (await through(libmime.createDecodeStream(node.charset || 'utf-8'), [decoded])).toString('utf8');
|
|
35
|
+
return node.flowed ? libmime.decodeFlowed(text, node.delSp) : text;
|
|
36
|
+
}
|
|
37
|
+
/** Splits the message into its nodes, each with the raw bytes of its body. */
|
|
38
|
+
async function split(source) {
|
|
39
|
+
// ignoreEmbedded keeps a forwarded original as one part instead of
|
|
40
|
+
// descending into it, which is the whole reason this module exists.
|
|
41
|
+
const splitter = new Splitter({ ignoreEmbedded: true });
|
|
42
|
+
const nodes = [];
|
|
43
|
+
let current;
|
|
44
|
+
const done = (async () => {
|
|
45
|
+
for await (const chunk of splitter) {
|
|
46
|
+
if (chunk.type === 'node') {
|
|
47
|
+
current = { node: chunk, body: [] };
|
|
48
|
+
nodes.push(current);
|
|
49
|
+
}
|
|
50
|
+
else if (chunk.type === 'body' && current && chunk.value) {
|
|
51
|
+
current.body.push(Buffer.from(chunk.value));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
})();
|
|
55
|
+
splitter.end(source);
|
|
56
|
+
await done;
|
|
57
|
+
return nodes;
|
|
58
|
+
}
|
|
59
|
+
/** Reads a stored message into its body and the files it carries. */
|
|
60
|
+
export async function readParts(source) {
|
|
61
|
+
const parts = { files: [] };
|
|
62
|
+
for (const { node, body } of await split(source)) {
|
|
63
|
+
// A multipart node is only a container; its children follow as nodes.
|
|
64
|
+
if (node.multipart)
|
|
65
|
+
continue;
|
|
66
|
+
const contentType = (node.contentType || 'text/plain').toLowerCase();
|
|
67
|
+
const decoded = await through(node.getDecoder(), body);
|
|
68
|
+
const isBody = node.disposition !== 'attachment' &&
|
|
69
|
+
!node.filename &&
|
|
70
|
+
(contentType === 'text/plain' || contentType === 'text/html');
|
|
71
|
+
if (isBody && contentType === 'text/html' && parts.html === undefined) {
|
|
72
|
+
parts.html = await asText(node, decoded);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (isBody && contentType === 'text/plain' && parts.text === undefined) {
|
|
76
|
+
parts.text = await asText(node, decoded);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const filename = node.filename || `part-${parts.files.length + 1}`;
|
|
80
|
+
// Proton attaches its own key to whatever it sends, so a key carried over
|
|
81
|
+
// from a stored draft would go out twice.
|
|
82
|
+
if (isProtonPublicKey(contentType, filename))
|
|
83
|
+
continue;
|
|
84
|
+
const id = node.headers ? node.headers.getFirst('content-id').trim() : '';
|
|
85
|
+
const file = { filename, contentType, content: decoded };
|
|
86
|
+
if (id)
|
|
87
|
+
file.contentId = id.replace(/^<|>$/g, '');
|
|
88
|
+
parts.files.push(file);
|
|
89
|
+
}
|
|
90
|
+
return parts;
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=parts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parts.js","sourceRoot":"","sources":["../../src/mime/parts.ts"],"names":[],"mappings":"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"}
|