@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
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attachments saved to this machine, and taking them away again.
|
|
3
|
+
*
|
|
4
|
+
* Saving means writing what Proton keeps encrypted, decrypted, to a disk. That
|
|
5
|
+
* is worth doing so an assistant can open a PDF or a spreadsheet rather than
|
|
6
|
+
* only say it exists, and it is bounded on every side:
|
|
7
|
+
*
|
|
8
|
+
* - **Only into a directory the user named**, in PROTON_MCP_ATTACHMENT_DIR, and
|
|
9
|
+
* only through the environment: a setting in the web interface would be
|
|
10
|
+
* within reach of an assistant, which gets that page's address. Without the
|
|
11
|
+
* variable nothing is written and the tool says how to set it up.
|
|
12
|
+
* - **Nothing outside it.** A folder is checked after it is resolved, `..` is
|
|
13
|
+
* refused, and so is a symbolic link anywhere on the way.
|
|
14
|
+
* - **The name comes from the sender** and is reduced to a plain one, with the
|
|
15
|
+
* rule in mime/filenames.ts. An existing file is never replaced: the file is
|
|
16
|
+
* created with O_EXCL, which also refuses to follow a link in its place.
|
|
17
|
+
* - **Readable by the user only**, 0600, and folders created here 0700.
|
|
18
|
+
* - **Gone again within the hour.** Decided by Tobi on 2026-11-02: a file lies
|
|
19
|
+
* unattended for at most one hour, and is deleted when the server stops.
|
|
20
|
+
* Better the assistant fetches it again than that it lies around for days.
|
|
21
|
+
* A server that was killed leaves its files to the next one to start.
|
|
22
|
+
*
|
|
23
|
+
* What may be deleted is recorded per process, in this server's own
|
|
24
|
+
* configuration directory rather than in the directory the files go to. Each
|
|
25
|
+
* running server writes only its own record, so two of them never overwrite
|
|
26
|
+
* each other's. Before anything is deleted it is checked to be the very file
|
|
27
|
+
* that was saved: the same inode, size and modification time, a plain file and
|
|
28
|
+
* not a link. A file replaced or changed since is left where it is, because in
|
|
29
|
+
* the directory there may be files that are not ours.
|
|
30
|
+
*
|
|
31
|
+
* A folder created for a file goes again once it is empty, decided by Tobi on
|
|
32
|
+
* 2026-11-02: its name can say what was in it. Recorded like the files, with
|
|
33
|
+
* its inode, and removed with rmdir, which takes an empty folder and nothing
|
|
34
|
+
* else. A folder that was there before is never recorded and never removed.
|
|
35
|
+
*/
|
|
36
|
+
import { createHash } from 'node:crypto';
|
|
37
|
+
import { closeSync, constants, fstatSync, lstatSync, mkdirSync, openSync, readdirSync, readFileSync, realpathSync, renameSync, rmdirSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync, } from 'node:fs';
|
|
38
|
+
import { homedir } from 'node:os';
|
|
39
|
+
import { basename, dirname, isAbsolute, join, parse as parsePath, relative, resolve, sep } from 'node:path';
|
|
40
|
+
import { BridgeError } from '../bridge/errors.js';
|
|
41
|
+
import { plainFileName } from '../mime/filenames.js';
|
|
42
|
+
/** How long a saved file may lie, fixed. */
|
|
43
|
+
export const KEEP_MS = 60 * 60 * 1000;
|
|
44
|
+
/** Proton accepts up to 25 MB per message, so no real attachment is larger. */
|
|
45
|
+
export const MAX_SAVE_BYTES = 25 * 1024 * 1024;
|
|
46
|
+
/** How long past its hour a file of a server that still runs is left to it. */
|
|
47
|
+
const GRACE_MS = 10 * 60 * 1000;
|
|
48
|
+
/** The variable that names the directory. */
|
|
49
|
+
export const ATTACHMENT_DIR_VARIABLE = 'PROTON_MCP_ATTACHMENT_DIR';
|
|
50
|
+
/** A refusal to save or delete, explained. Reported like any explained failure. */
|
|
51
|
+
export class SaveRefused extends BridgeError {
|
|
52
|
+
name = 'SaveRefused';
|
|
53
|
+
}
|
|
54
|
+
/** Where the records live unless told otherwise. */
|
|
55
|
+
export function defaultRecordDir() {
|
|
56
|
+
return join(homedir(), '.config', 'proton-mcp', 'saved-attachments');
|
|
57
|
+
}
|
|
58
|
+
function processAlive(pid) {
|
|
59
|
+
try {
|
|
60
|
+
process.kill(pid, 0);
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
// EPERM: it exists, it is merely not ours to signal.
|
|
65
|
+
return error.code === 'EPERM';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** A file whose identity no longer matches the record is not ours to delete. */
|
|
69
|
+
function deleteIfSame(entry) {
|
|
70
|
+
let info;
|
|
71
|
+
try {
|
|
72
|
+
info = lstatSync(entry.path);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return 'missing';
|
|
76
|
+
}
|
|
77
|
+
const same = info.isFile() &&
|
|
78
|
+
info.dev === entry.dev &&
|
|
79
|
+
info.ino === entry.ino &&
|
|
80
|
+
info.size === entry.size &&
|
|
81
|
+
info.mtimeMs === entry.mtimeMs;
|
|
82
|
+
if (!same)
|
|
83
|
+
return 'left';
|
|
84
|
+
try {
|
|
85
|
+
unlinkSync(entry.path);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// Not deletable, permissions for instance. Left and reported rather than
|
|
89
|
+
// letting one file stop the clearing up of all the others.
|
|
90
|
+
return 'left';
|
|
91
|
+
}
|
|
92
|
+
return 'deleted';
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Removes the folders this server created that are empty now, deepest first,
|
|
96
|
+
* so that a parent can follow its child.
|
|
97
|
+
*
|
|
98
|
+
* rmdir and nothing stronger: whatever someone put into a folder keeps it
|
|
99
|
+
* standing. And only while it is still the folder that was created, the same
|
|
100
|
+
* inode, never a link in its place. Returns what went and what is left to
|
|
101
|
+
* watch; a folder that is gone or no longer ours is neither.
|
|
102
|
+
*
|
|
103
|
+
* The limit of that: a folder deleted and made again under the same name can
|
|
104
|
+
* get the same inode back, as it did on the CI's file system on 2026-11-02,
|
|
105
|
+
* and is then taken for the one created here. rmdir removes it only if it is
|
|
106
|
+
* empty. The time a folder was made would tell them apart, but where the file
|
|
107
|
+
* system does not keep it, Node reports the ctime instead, which changes as
|
|
108
|
+
* soon as a file inside goes, and no folder would ever be removed.
|
|
109
|
+
*/
|
|
110
|
+
function pruneFolders(folders) {
|
|
111
|
+
const removed = [];
|
|
112
|
+
const kept = [];
|
|
113
|
+
const deepestFirst = [...folders].sort((a, b) => b.path.split(sep).length - a.path.split(sep).length);
|
|
114
|
+
for (const folder of deepestFirst) {
|
|
115
|
+
let info;
|
|
116
|
+
try {
|
|
117
|
+
info = lstatSync(folder.path);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (!info.isDirectory() || info.dev !== folder.dev || info.ino !== folder.ino)
|
|
123
|
+
continue;
|
|
124
|
+
try {
|
|
125
|
+
rmdirSync(folder.path);
|
|
126
|
+
removed.push(folder.path);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// Not empty, or not removable: watched until this server stops.
|
|
130
|
+
kept.push(folder);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return { removed, kept };
|
|
134
|
+
}
|
|
135
|
+
export class SavedAttachments {
|
|
136
|
+
#root;
|
|
137
|
+
#recordDir;
|
|
138
|
+
#now;
|
|
139
|
+
#pid;
|
|
140
|
+
#isAlive;
|
|
141
|
+
#startedAt;
|
|
142
|
+
#timers = new Map();
|
|
143
|
+
#entries = [];
|
|
144
|
+
#folders = [];
|
|
145
|
+
constructor(options) {
|
|
146
|
+
this.#root = options.root?.trim() || undefined;
|
|
147
|
+
this.#recordDir = options.recordDir ?? defaultRecordDir();
|
|
148
|
+
this.#now = options.now ?? Date.now;
|
|
149
|
+
this.#pid = options.pid ?? process.pid;
|
|
150
|
+
this.#isAlive = options.isAlive ?? processAlive;
|
|
151
|
+
this.#startedAt = new Date(this.#now()).toISOString();
|
|
152
|
+
}
|
|
153
|
+
/** Whether the user set a directory at all. */
|
|
154
|
+
get configured() {
|
|
155
|
+
return this.#root !== undefined;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Throws the explanation when nothing can be saved: no directory set, or one
|
|
159
|
+
* that is not there. Called before a message is fetched, so that nothing is
|
|
160
|
+
* decrypted only to be thrown away.
|
|
161
|
+
*/
|
|
162
|
+
assertUsable() {
|
|
163
|
+
this.#realRoot();
|
|
164
|
+
}
|
|
165
|
+
/** This process's own record, named so that no two processes share one. */
|
|
166
|
+
get #recordPath() {
|
|
167
|
+
return join(this.#recordDir, `${this.#pid}-${Date.parse(this.#startedAt)}.json`);
|
|
168
|
+
}
|
|
169
|
+
/** The directory, resolved, or the reason it cannot be used. */
|
|
170
|
+
#realRoot() {
|
|
171
|
+
const root = this.#root;
|
|
172
|
+
if (root === undefined) {
|
|
173
|
+
throw new SaveRefused(`Saving attachments is not set up. The user has to set ${ATTACHMENT_DIR_VARIABLE} to a ` +
|
|
174
|
+
'directory, in the configuration this MCP server is started with, and restart the client. ' +
|
|
175
|
+
'Files are saved there and nowhere else. Until then an attachment can only be read with ' +
|
|
176
|
+
'get_attachment, and only when it is text.');
|
|
177
|
+
}
|
|
178
|
+
if (!isAbsolute(root)) {
|
|
179
|
+
throw new SaveRefused(`${ATTACHMENT_DIR_VARIABLE} has to be an absolute path, and "${root}" is not.`);
|
|
180
|
+
}
|
|
181
|
+
let real;
|
|
182
|
+
try {
|
|
183
|
+
real = realpathSync(root);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
throw new SaveRefused(`${ATTACHMENT_DIR_VARIABLE} names "${root}", which does not exist. It is not created ` +
|
|
187
|
+
'here, so that a mistyped path does not quietly become a new place for mail to land.');
|
|
188
|
+
}
|
|
189
|
+
if (!statSync(real).isDirectory()) {
|
|
190
|
+
throw new SaveRefused(`${ATTACHMENT_DIR_VARIABLE} names "${root}", which is not a directory.`);
|
|
191
|
+
}
|
|
192
|
+
return real;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* The folder to write into, inside the directory and nowhere else.
|
|
196
|
+
*
|
|
197
|
+
* Missing folders are created one level at a time, each checked before the
|
|
198
|
+
* next, so that no folder is ever created through a link pointing out.
|
|
199
|
+
*/
|
|
200
|
+
#targetDir(folder) {
|
|
201
|
+
const real = this.#realRoot();
|
|
202
|
+
if (!folder)
|
|
203
|
+
return real;
|
|
204
|
+
const requested = resolve(isAbsolute(folder) ? folder : join(real, folder));
|
|
205
|
+
// Compared with the directory as given and as resolved: a directory that
|
|
206
|
+
// is itself reached through a link reads differently from either side.
|
|
207
|
+
const inside = [this.#root, real]
|
|
208
|
+
.map((base) => relative(base, requested))
|
|
209
|
+
.find((rel) => rel === '' || (!rel.startsWith('..') && !isAbsolute(rel)));
|
|
210
|
+
if (inside === undefined) {
|
|
211
|
+
throw new SaveRefused(`"${folder}" is outside the directory set for attachments, ${this.#root}. Files are saved ` +
|
|
212
|
+
'there or in a folder inside it, never anywhere else.');
|
|
213
|
+
}
|
|
214
|
+
let current = real;
|
|
215
|
+
for (const part of inside.split(sep).filter(Boolean)) {
|
|
216
|
+
current = join(current, part);
|
|
217
|
+
let info;
|
|
218
|
+
try {
|
|
219
|
+
info = lstatSync(current);
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
mkdirSync(current, { mode: 0o700 });
|
|
223
|
+
const made = lstatSync(current);
|
|
224
|
+
this.#folders.push({ path: current, dev: made.dev, ino: made.ino });
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (info.isSymbolicLink()) {
|
|
228
|
+
throw new SaveRefused(`"${current}" is a symbolic link, and a folder reached through one is refused.`);
|
|
229
|
+
}
|
|
230
|
+
if (!info.isDirectory())
|
|
231
|
+
throw new SaveRefused(`"${current}" is not a folder.`);
|
|
232
|
+
}
|
|
233
|
+
return current;
|
|
234
|
+
}
|
|
235
|
+
/** Writes the record of this process, atomically, or removes it once empty. */
|
|
236
|
+
#writeRecord() {
|
|
237
|
+
if (this.#entries.length === 0 && this.#folders.length === 0) {
|
|
238
|
+
rmSync(this.#recordPath, { force: true });
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
mkdirSync(this.#recordDir, { recursive: true, mode: 0o700 });
|
|
242
|
+
const record = {
|
|
243
|
+
pid: this.#pid,
|
|
244
|
+
startedAt: this.#startedAt,
|
|
245
|
+
entries: this.#entries,
|
|
246
|
+
folders: this.#folders,
|
|
247
|
+
};
|
|
248
|
+
const temporary = `${this.#recordPath}.tmp`;
|
|
249
|
+
writeFileSync(temporary, `${JSON.stringify(record, null, 2)}\n`, { mode: 0o600 });
|
|
250
|
+
renameSync(temporary, this.#recordPath);
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Saves one attachment. Synchronous on purpose: the file and its record are
|
|
254
|
+
* written in one step, with nothing in between that could stop halfway.
|
|
255
|
+
*/
|
|
256
|
+
save(name, content, contentType, folder) {
|
|
257
|
+
if (content.length > MAX_SAVE_BYTES) {
|
|
258
|
+
throw new SaveRefused(`The attachment is ${content.length} bytes, more than the ${MAX_SAVE_BYTES} this server saves.`);
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
return this.#write(name, content, contentType, folder);
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
// A folder made for a file that never arrived is not left behind.
|
|
265
|
+
this.#settleFolders();
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
#write(name, content, contentType, folder) {
|
|
270
|
+
const directory = this.#targetDir(folder);
|
|
271
|
+
const plain = plainFileName(name, 'attachment');
|
|
272
|
+
const { name: stem, ext } = parsePath(plain);
|
|
273
|
+
for (let n = 1; n <= 100; n++) {
|
|
274
|
+
const path = join(directory, n === 1 ? plain : `${stem} (${n})${ext}`);
|
|
275
|
+
let fd;
|
|
276
|
+
try {
|
|
277
|
+
// O_EXCL: never an existing file, and never through a link in its place.
|
|
278
|
+
fd = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 0o600);
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
if (error.code === 'EEXIST')
|
|
282
|
+
continue;
|
|
283
|
+
throw error;
|
|
284
|
+
}
|
|
285
|
+
let ino;
|
|
286
|
+
try {
|
|
287
|
+
writeSync(fd, content);
|
|
288
|
+
ino = fstatSync(fd).ino;
|
|
289
|
+
}
|
|
290
|
+
finally {
|
|
291
|
+
closeSync(fd);
|
|
292
|
+
}
|
|
293
|
+
const info = lstatSync(path);
|
|
294
|
+
if (info.ino !== ino)
|
|
295
|
+
throw new SaveRefused(`"${path}" was replaced while it was being written.`);
|
|
296
|
+
const savedAt = this.#now();
|
|
297
|
+
const expiresAt = savedAt + KEEP_MS;
|
|
298
|
+
this.#entries.push({
|
|
299
|
+
path,
|
|
300
|
+
dev: info.dev,
|
|
301
|
+
ino: info.ino,
|
|
302
|
+
size: info.size,
|
|
303
|
+
mtimeMs: info.mtimeMs,
|
|
304
|
+
savedAt: new Date(savedAt).toISOString(),
|
|
305
|
+
expiresAt: new Date(expiresAt).toISOString(),
|
|
306
|
+
});
|
|
307
|
+
this.#writeRecord();
|
|
308
|
+
// Exactly on the hour rather than on the next sweep. Unref'd, so a
|
|
309
|
+
// waiting deletion never keeps the process alive.
|
|
310
|
+
this.#timers.set(path, setTimeout(() => this.#expire(path), KEEP_MS).unref());
|
|
311
|
+
return {
|
|
312
|
+
path,
|
|
313
|
+
size: info.size,
|
|
314
|
+
contentType,
|
|
315
|
+
sha256: createHash('sha256').update(content).digest('hex'),
|
|
316
|
+
expiresAt: new Date(expiresAt),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
throw new SaveRefused(`There are already a hundred files named like "${plain}" in "${directory}".`);
|
|
320
|
+
}
|
|
321
|
+
/** Deletes one file whose hour is up, if it is still the one that was saved. */
|
|
322
|
+
#expire(path) {
|
|
323
|
+
this.#timers.delete(path);
|
|
324
|
+
const entry = this.#entries.find((e) => e.path === path);
|
|
325
|
+
if (!entry)
|
|
326
|
+
return;
|
|
327
|
+
deleteIfSame(entry);
|
|
328
|
+
this.#entries = this.#entries.filter((e) => e !== entry);
|
|
329
|
+
this.#settleFolders();
|
|
330
|
+
}
|
|
331
|
+
/** Removes the empty ones among the folders created here, and writes the record. */
|
|
332
|
+
#settleFolders() {
|
|
333
|
+
const { removed, kept } = pruneFolders(this.#folders);
|
|
334
|
+
this.#folders = kept;
|
|
335
|
+
this.#writeRecord();
|
|
336
|
+
return removed;
|
|
337
|
+
}
|
|
338
|
+
/** The record a path stands for, read the way save wrote it. */
|
|
339
|
+
#entryFor(path) {
|
|
340
|
+
const absolute = resolve(path);
|
|
341
|
+
const exact = this.#entries.find((e) => e.path === absolute);
|
|
342
|
+
if (exact)
|
|
343
|
+
return exact;
|
|
344
|
+
// The same file reached through a link in its folder's path.
|
|
345
|
+
try {
|
|
346
|
+
const real = join(realpathSync(dirname(absolute)), basename(absolute));
|
|
347
|
+
return this.#entries.find((e) => e.path === real);
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
return undefined;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Deletes what this server saved: everything, or the files named.
|
|
355
|
+
*
|
|
356
|
+
* A path it did not save is not touched, whatever it is.
|
|
357
|
+
*/
|
|
358
|
+
delete(paths) {
|
|
359
|
+
const report = { deleted: [], left: [], missing: [], unknown: [], folders: [] };
|
|
360
|
+
const chosen = [];
|
|
361
|
+
if (paths === undefined) {
|
|
362
|
+
chosen.push(...this.#entries);
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
for (const path of paths) {
|
|
366
|
+
const entry = this.#entryFor(path);
|
|
367
|
+
if (entry)
|
|
368
|
+
chosen.push(entry);
|
|
369
|
+
else
|
|
370
|
+
report.unknown.push(path);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
for (const entry of chosen) {
|
|
374
|
+
const outcome = deleteIfSame(entry);
|
|
375
|
+
report[outcome === 'deleted' ? 'deleted' : outcome === 'left' ? 'left' : 'missing'].push(entry.path);
|
|
376
|
+
clearTimeout(this.#timers.get(entry.path));
|
|
377
|
+
this.#timers.delete(entry.path);
|
|
378
|
+
}
|
|
379
|
+
this.#entries = this.#entries.filter((e) => !chosen.includes(e));
|
|
380
|
+
report.folders = this.#settleFolders();
|
|
381
|
+
return report;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Clears up after other servers: everything of one that no longer runs, and
|
|
385
|
+
* what one that still runs has left lying well past its hour.
|
|
386
|
+
*/
|
|
387
|
+
sweepOthers() {
|
|
388
|
+
let names;
|
|
389
|
+
try {
|
|
390
|
+
names = readdirSync(this.#recordDir);
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const own = basename(this.#recordPath);
|
|
396
|
+
for (const name of names) {
|
|
397
|
+
if (!name.endsWith('.json') || name === own)
|
|
398
|
+
continue;
|
|
399
|
+
const path = join(this.#recordDir, name);
|
|
400
|
+
let record;
|
|
401
|
+
try {
|
|
402
|
+
record = JSON.parse(readFileSync(path, 'utf8'));
|
|
403
|
+
}
|
|
404
|
+
catch {
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
const dead = !this.#isAlive(record.pid);
|
|
408
|
+
const keep = [];
|
|
409
|
+
for (const entry of record.entries ?? []) {
|
|
410
|
+
const stale = this.#now() > Date.parse(entry.expiresAt) + GRACE_MS;
|
|
411
|
+
if (dead || stale)
|
|
412
|
+
deleteIfSame(entry);
|
|
413
|
+
else
|
|
414
|
+
keep.push(entry);
|
|
415
|
+
}
|
|
416
|
+
// The folders of a server that still runs stay its own business: it may
|
|
417
|
+
// be about to save into one of them.
|
|
418
|
+
if (dead)
|
|
419
|
+
pruneFolders(record.folders ?? []);
|
|
420
|
+
if (keep.length === 0) {
|
|
421
|
+
rmSync(path, { force: true });
|
|
422
|
+
}
|
|
423
|
+
else if (keep.length !== (record.entries ?? []).length) {
|
|
424
|
+
const temporary = `${path}.tmp`;
|
|
425
|
+
writeFileSync(temporary, `${JSON.stringify({ ...record, entries: keep }, null, 2)}\n`, { mode: 0o600 });
|
|
426
|
+
renameSync(temporary, path);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
/** Called once at start: clears up what earlier servers left. */
|
|
431
|
+
start() {
|
|
432
|
+
this.sweepOthers();
|
|
433
|
+
}
|
|
434
|
+
/** Called on the way out: deletes everything this server saved. */
|
|
435
|
+
stop() {
|
|
436
|
+
for (const timer of this.#timers.values())
|
|
437
|
+
clearTimeout(timer);
|
|
438
|
+
this.#timers.clear();
|
|
439
|
+
if (this.#entries.length || this.#folders.length)
|
|
440
|
+
this.delete();
|
|
441
|
+
rmSync(this.#recordPath, { force: true });
|
|
442
|
+
}
|
|
443
|
+
/** What this server has saved and not yet deleted. */
|
|
444
|
+
get entries() {
|
|
445
|
+
return this.#entries;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
//# sourceMappingURL=saved-attachments.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"saved-attachments.js","sourceRoot":"","sources":["../../src/mail/saved-attachments.ts"],"names":[],"mappings":"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"}
|
package/dist/mail/search.d.ts
CHANGED
|
@@ -12,10 +12,32 @@
|
|
|
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 type { SearchObject } from 'imapflow';
|
|
17
29
|
import type { Connection } from '../bridge/connection.js';
|
|
18
|
-
import { type MessageHeader, type OrderingCost } from './messages.js';
|
|
30
|
+
import { type EnvelopeEntry, type MessageHeader, type OrderingCost } 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 declare const MAX_COMPARED = 20000;
|
|
19
41
|
export interface SearchCriteria {
|
|
20
42
|
/** Free text, searched in the body and in headers. The expensive part. */
|
|
21
43
|
text?: string;
|
|
@@ -58,9 +80,51 @@ export interface SearchResult {
|
|
|
58
80
|
ordering: OrderingCost;
|
|
59
81
|
/** True when the search included a full-text term, which is the slow case. */
|
|
60
82
|
fullText: boolean;
|
|
83
|
+
/** True when a full-text word was also asked for in its quoted-printable form. */
|
|
84
|
+
encodedForms: boolean;
|
|
85
|
+
/** Set when not every candidate was compared, see MAX_COMPARED. */
|
|
86
|
+
compared?: {
|
|
87
|
+
compared: number;
|
|
88
|
+
of: number;
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/** What this server compares itself, against the decoded envelope. */
|
|
92
|
+
export interface LocalMatch {
|
|
93
|
+
subject?: string;
|
|
94
|
+
from?: string;
|
|
95
|
+
to?: string;
|
|
61
96
|
}
|
|
62
|
-
/**
|
|
63
|
-
export
|
|
97
|
+
/** What the Bridge is asked, and what is left to compare here. */
|
|
98
|
+
export interface PlannedSearch {
|
|
99
|
+
query: SearchObject;
|
|
100
|
+
local: LocalMatch;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* A word as a quoted-printable part carries it: every byte of its UTF-8 form
|
|
104
|
+
* outside printable ASCII, and the equals sign, as =XX.
|
|
105
|
+
*/
|
|
106
|
+
export declare function quotedPrintable(text: string): string;
|
|
107
|
+
/**
|
|
108
|
+
* The forms a full-text word is asked for in.
|
|
109
|
+
*
|
|
110
|
+
* The Bridge compares the raw bytes of a message, lower cased, so a word with a
|
|
111
|
+
* letter outside ASCII is found only where a part carries it unencoded. Most
|
|
112
|
+
* text with umlauts travels as quoted-printable, where a u with umlaut is the
|
|
113
|
+
* six characters "=C3=BC" and an a with umlaut "=C3=A4". Lower casing does not reach into
|
|
114
|
+
* that encoding, so the usual spellings of a word are each asked for: as given,
|
|
115
|
+
* in lower case, and capitalised. Forms that differ only in the case of ASCII
|
|
116
|
+
* letters are asked once, since the Bridge lower cases both sides. A word
|
|
117
|
+
* broken across a line by the encoding is still missed, and so is anything in
|
|
118
|
+
* base64.
|
|
119
|
+
*/
|
|
120
|
+
export declare function textForms(term: string): string[];
|
|
121
|
+
/** Whether a message passes what this server compares itself. */
|
|
122
|
+
export declare function matchesLocally(entry: EnvelopeEntry, local: LocalMatch): boolean;
|
|
123
|
+
/**
|
|
124
|
+
* Splits the criteria into what the Bridge is asked and what this server
|
|
125
|
+
* compares itself.
|
|
126
|
+
*/
|
|
127
|
+
export declare function planSearch(criteria: SearchCriteria): PlannedSearch;
|
|
64
128
|
/** Whether a query contains the expensive full-text part. */
|
|
65
129
|
export declare function isFullText(criteria: SearchCriteria): boolean;
|
|
66
130
|
export interface SearchOptions extends SearchCriteria {
|