@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.
- package/LICENSE +20 -0
- package/README.md +405 -0
- package/dist/activity.d.ts +49 -0
- package/dist/activity.js +72 -0
- package/dist/activity.js.map +1 -0
- package/dist/bridge/certificate.d.ts +75 -0
- package/dist/bridge/certificate.js +176 -0
- package/dist/bridge/certificate.js.map +1 -0
- package/dist/bridge/connection.d.ts +99 -0
- package/dist/bridge/connection.js +244 -0
- package/dist/bridge/connection.js.map +1 -0
- package/dist/bridge/errors.d.ts +35 -0
- package/dist/bridge/errors.js +56 -0
- package/dist/bridge/errors.js.map +1 -0
- package/dist/config.d.ts +92 -0
- package/dist/config.js +142 -0
- package/dist/config.js.map +1 -0
- package/dist/credentials/availability.d.ts +62 -0
- package/dist/credentials/availability.js +107 -0
- package/dist/credentials/availability.js.map +1 -0
- package/dist/credentials/encrypted-file.d.ts +50 -0
- package/dist/credentials/encrypted-file.js +156 -0
- package/dist/credentials/encrypted-file.js.map +1 -0
- package/dist/credentials/keyring.d.ts +48 -0
- package/dist/credentials/keyring.js +97 -0
- package/dist/credentials/keyring.js.map +1 -0
- package/dist/credentials/plain-file.d.ts +24 -0
- package/dist/credentials/plain-file.js +78 -0
- package/dist/credentials/plain-file.js.map +1 -0
- package/dist/credentials/probe-keyring.d.ts +11 -0
- package/dist/credentials/probe-keyring.js +57 -0
- package/dist/credentials/probe-keyring.js.map +1 -0
- package/dist/credentials/session.d.ts +15 -0
- package/dist/credentials/session.js +21 -0
- package/dist/credentials/session.js.map +1 -0
- package/dist/credentials/store.d.ts +80 -0
- package/dist/credentials/store.js +94 -0
- package/dist/credentials/store.js.map +1 -0
- package/dist/in-flight.d.ts +33 -0
- package/dist/in-flight.js +78 -0
- package/dist/in-flight.js.map +1 -0
- package/dist/mail/actions.d.ts +116 -0
- package/dist/mail/actions.js +265 -0
- package/dist/mail/actions.js.map +1 -0
- package/dist/mail/attachments.d.ts +39 -0
- package/dist/mail/attachments.js +86 -0
- package/dist/mail/attachments.js.map +1 -0
- package/dist/mail/compose.d.ts +264 -0
- package/dist/mail/compose.js +395 -0
- package/dist/mail/compose.js.map +1 -0
- package/dist/mail/drafts.d.ts +138 -0
- package/dist/mail/drafts.js +449 -0
- package/dist/mail/drafts.js.map +1 -0
- package/dist/mail/ids.d.ts +78 -0
- package/dist/mail/ids.js +135 -0
- package/dist/mail/ids.js.map +1 -0
- package/dist/mail/labels.d.ts +61 -0
- package/dist/mail/labels.js +139 -0
- package/dist/mail/labels.js.map +1 -0
- package/dist/mail/markup.d.ts +91 -0
- package/dist/mail/markup.js +265 -0
- package/dist/mail/markup.js.map +1 -0
- package/dist/mail/messages.d.ts +147 -0
- package/dist/mail/messages.js +219 -0
- package/dist/mail/messages.js.map +1 -0
- package/dist/mail/quote.d.ts +65 -0
- package/dist/mail/quote.js +157 -0
- package/dist/mail/quote.js.map +1 -0
- package/dist/mail/search.d.ts +66 -0
- package/dist/mail/search.js +107 -0
- package/dist/mail/search.js.map +1 -0
- package/dist/mail/send.d.ts +61 -0
- package/dist/mail/send.js +132 -0
- package/dist/mail/send.js.map +1 -0
- package/dist/mime/parse.d.ts +110 -0
- package/dist/mime/parse.js +194 -0
- package/dist/mime/parse.js.map +1 -0
- package/dist/server.d.ts +13 -0
- package/dist/server.js +387 -0
- package/dist/server.js.map +1 -0
- package/dist/session.d.ts +86 -0
- package/dist/session.js +220 -0
- package/dist/session.js.map +1 -0
- package/dist/settings.d.ts +52 -0
- package/dist/settings.js +80 -0
- package/dist/settings.js.map +1 -0
- package/dist/tools/actions.d.ts +21 -0
- package/dist/tools/actions.js +133 -0
- package/dist/tools/actions.js.map +1 -0
- package/dist/tools/capabilities.d.ts +52 -0
- package/dist/tools/capabilities.js +96 -0
- package/dist/tools/capabilities.js.map +1 -0
- package/dist/tools/confirm.d.ts +84 -0
- package/dist/tools/confirm.js +212 -0
- package/dist/tools/confirm.js.map +1 -0
- package/dist/tools/drafts.d.ts +21 -0
- package/dist/tools/drafts.js +199 -0
- package/dist/tools/drafts.js.map +1 -0
- package/dist/tools/failures.d.ts +46 -0
- package/dist/tools/failures.js +157 -0
- package/dist/tools/failures.js.map +1 -0
- package/dist/tools/format.d.ts +55 -0
- package/dist/tools/format.js +220 -0
- package/dist/tools/format.js.map +1 -0
- package/dist/tools/guidance.d.ts +38 -0
- package/dist/tools/guidance.js +226 -0
- package/dist/tools/guidance.js.map +1 -0
- package/dist/tools/interface.d.ts +28 -0
- package/dist/tools/interface.js +63 -0
- package/dist/tools/interface.js.map +1 -0
- package/dist/tools/labels.d.ts +21 -0
- package/dist/tools/labels.js +92 -0
- package/dist/tools/labels.js.map +1 -0
- package/dist/tools/list-folders.d.ts +14 -0
- package/dist/tools/list-folders.js +70 -0
- package/dist/tools/list-folders.js.map +1 -0
- package/dist/tools/messages.d.ts +9 -0
- package/dist/tools/messages.js +221 -0
- package/dist/tools/messages.js.map +1 -0
- package/dist/tools/send.d.ts +29 -0
- package/dist/tools/send.js +236 -0
- package/dist/tools/send.js.map +1 -0
- package/dist/tools/sign-in-required.d.ts +94 -0
- package/dist/tools/sign-in-required.js +168 -0
- package/dist/tools/sign-in-required.js.map +1 -0
- package/dist/web/layout.d.ts +75 -0
- package/dist/web/layout.js +590 -0
- package/dist/web/layout.js.map +1 -0
- package/dist/web/pages.d.ts +79 -0
- package/dist/web/pages.js +284 -0
- package/dist/web/pages.js.map +1 -0
- package/dist/web/sections.d.ts +105 -0
- package/dist/web/sections.js +207 -0
- package/dist/web/sections.js.map +1 -0
- package/dist/web/security.d.ts +122 -0
- package/dist/web/security.js +180 -0
- package/dist/web/security.js.map +1 -0
- package/dist/web/server.d.ts +153 -0
- package/dist/web/server.js +622 -0
- package/dist/web/server.js.map +1 -0
- package/package.json +70 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable message identifiers.
|
|
3
|
+
*
|
|
4
|
+
* Measured against the Bridge: UIDs are per mailbox and change when a message
|
|
5
|
+
* is moved, while the Message-ID stays stable. A UID is therefore a throwaway
|
|
6
|
+
* address and never handed out as an identifier.
|
|
7
|
+
*
|
|
8
|
+
* Every identifier this server hands out is the Message-ID. Resolving it back
|
|
9
|
+
* to a UID happens on demand, which costs a search but survives a message
|
|
10
|
+
* being moved between two calls of a conversation.
|
|
11
|
+
*/
|
|
12
|
+
import type { ImapFlow } from 'imapflow';
|
|
13
|
+
import type { Connection } from '../bridge/connection.js';
|
|
14
|
+
/** Mailbox that holds every message, including trash. The fallback for lookups. */
|
|
15
|
+
export declare const ALL_MAIL = "All Mail";
|
|
16
|
+
/**
|
|
17
|
+
* Brings a Message-ID into the shape the IMAP header uses.
|
|
18
|
+
*
|
|
19
|
+
* Callers paste identifiers from earlier answers, sometimes without the angle
|
|
20
|
+
* brackets. Both forms have to work, otherwise a lookup fails for a reason
|
|
21
|
+
* nobody can see.
|
|
22
|
+
*/
|
|
23
|
+
export declare function normaliseMessageId(id: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* The identifier as this server hands it out.
|
|
26
|
+
*
|
|
27
|
+
* Every answer uses this, so that the same message has the same name whichever
|
|
28
|
+
* tool named it. It did not, and the difference was invisible until it was not:
|
|
29
|
+
* a listing reads the IMAP envelope, which repeats the header as it stands,
|
|
30
|
+
* while reading a message goes through a MIME parser that adds the brackets RFC
|
|
31
|
+
* 5322 requires. For a header written correctly both agree. For one written
|
|
32
|
+
* without brackets the same message came back under two names.
|
|
33
|
+
*
|
|
34
|
+
* An empty identifier stays empty rather than becoming `<>`. A message without
|
|
35
|
+
* one cannot be addressed at all, and pretending otherwise would only move the
|
|
36
|
+
* failure further away from its cause.
|
|
37
|
+
*/
|
|
38
|
+
export declare function presentMessageId(raw: string): string;
|
|
39
|
+
export interface ResolvedMessage {
|
|
40
|
+
messageId: string;
|
|
41
|
+
/** The mailbox the message was found in. */
|
|
42
|
+
path: string;
|
|
43
|
+
/** Valid only together with `path`, and only until the message is moved. */
|
|
44
|
+
uid: number;
|
|
45
|
+
/** Changes when the Bridge renumbers UIDs. Cached UIDs are void afterwards. */
|
|
46
|
+
uidValidity: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Searches one mailbox for a Message-ID and returns the UID if present.
|
|
50
|
+
*
|
|
51
|
+
* Two attempts, and the second one exists because of a real message.
|
|
52
|
+
*
|
|
53
|
+
* RFC 5322 requires the angle brackets, so `normaliseMessageId` puts them back
|
|
54
|
+
* on and the first search asks for the bracketed form. That search is exact in
|
|
55
|
+
* practice even though the comparison is a substring one: `<abc>` cannot occur
|
|
56
|
+
* inside `<xyzabc>`, because the bracket has to sit immediately before the
|
|
57
|
+
* identifier.
|
|
58
|
+
*
|
|
59
|
+
* Real mail does not always obey. A message was found whose header carries the
|
|
60
|
+
* identifier bare, and the envelope hands it out exactly as it stands, so
|
|
61
|
+
* everything this server had already reported for that message was unfindable:
|
|
62
|
+
* the bracketed search never matched, and it was the only search there was.
|
|
63
|
+
* Marking 67 messages as read left that one behind with no way to reach it.
|
|
64
|
+
*
|
|
65
|
+
* The second attempt therefore drops the brackets, which finds both spellings,
|
|
66
|
+
* and every hit is verified because that search can also match a message that
|
|
67
|
+
* merely contains the identifier. It only runs when the first one came back
|
|
68
|
+
* empty, so nothing normal pays for it.
|
|
69
|
+
*/
|
|
70
|
+
export declare function findByMessageId(client: ImapFlow, messageId: string): Promise<number | undefined>;
|
|
71
|
+
/**
|
|
72
|
+
* Resolves a Message-ID to a mailbox and UID.
|
|
73
|
+
*
|
|
74
|
+
* `hint` is a mailbox to try first. It is a shortcut, not a requirement: if the
|
|
75
|
+
* message is not there, the search falls back to "All Mail", which holds
|
|
76
|
+
* everything. That is what makes the identifier survive a move.
|
|
77
|
+
*/
|
|
78
|
+
export declare function resolveMessageId(connection: Connection, rawId: string, hint?: string): Promise<ResolvedMessage>;
|
package/dist/mail/ids.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable message identifiers.
|
|
3
|
+
*
|
|
4
|
+
* Measured against the Bridge: UIDs are per mailbox and change when a message
|
|
5
|
+
* is moved, while the Message-ID stays stable. A UID is therefore a throwaway
|
|
6
|
+
* address and never handed out as an identifier.
|
|
7
|
+
*
|
|
8
|
+
* Every identifier this server hands out is the Message-ID. Resolving it back
|
|
9
|
+
* to a UID happens on demand, which costs a search but survives a message
|
|
10
|
+
* being moved between two calls of a conversation.
|
|
11
|
+
*/
|
|
12
|
+
import { BridgeError } from '../bridge/errors.js';
|
|
13
|
+
/** Mailbox that holds every message, including trash. The fallback for lookups. */
|
|
14
|
+
export const ALL_MAIL = 'All Mail';
|
|
15
|
+
/**
|
|
16
|
+
* Brings a Message-ID into the shape the IMAP header uses.
|
|
17
|
+
*
|
|
18
|
+
* Callers paste identifiers from earlier answers, sometimes without the angle
|
|
19
|
+
* brackets. Both forms have to work, otherwise a lookup fails for a reason
|
|
20
|
+
* nobody can see.
|
|
21
|
+
*/
|
|
22
|
+
export function normaliseMessageId(id) {
|
|
23
|
+
const trimmed = id.trim();
|
|
24
|
+
if (!trimmed)
|
|
25
|
+
throw new BridgeError('An empty message id cannot be resolved.');
|
|
26
|
+
const bare = trimmed.replace(/^<|>$/g, '');
|
|
27
|
+
return `<${bare}>`;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The identifier as this server hands it out.
|
|
31
|
+
*
|
|
32
|
+
* Every answer uses this, so that the same message has the same name whichever
|
|
33
|
+
* tool named it. It did not, and the difference was invisible until it was not:
|
|
34
|
+
* a listing reads the IMAP envelope, which repeats the header as it stands,
|
|
35
|
+
* while reading a message goes through a MIME parser that adds the brackets RFC
|
|
36
|
+
* 5322 requires. For a header written correctly both agree. For one written
|
|
37
|
+
* without brackets the same message came back under two names.
|
|
38
|
+
*
|
|
39
|
+
* An empty identifier stays empty rather than becoming `<>`. A message without
|
|
40
|
+
* one cannot be addressed at all, and pretending otherwise would only move the
|
|
41
|
+
* failure further away from its cause.
|
|
42
|
+
*/
|
|
43
|
+
export function presentMessageId(raw) {
|
|
44
|
+
const trimmed = raw.trim();
|
|
45
|
+
return trimmed ? normaliseMessageId(trimmed) : '';
|
|
46
|
+
}
|
|
47
|
+
/** The identifier without its angle brackets, whether it had any or not. */
|
|
48
|
+
function bareForm(messageId) {
|
|
49
|
+
return messageId.replace(/^<|>$/g, '');
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Checks that a hit really carries this identifier.
|
|
53
|
+
*
|
|
54
|
+
* Needed because the IMAP header search is a **substring** comparison, measured
|
|
55
|
+
* against the Bridge on 31.07.2026: searching for a fragment of an identifier
|
|
56
|
+
* returns the message. So a search for `abc@example.com` also matches a message
|
|
57
|
+
* whose header reads `<xyzabc@example.com>`, and taking that hit would mark the
|
|
58
|
+
* wrong message. Silently, which is the part that matters for a write.
|
|
59
|
+
*
|
|
60
|
+
* The envelope is read rather than the raw header, because that is the same
|
|
61
|
+
* source list_messages hands its identifiers out from. Comparing against
|
|
62
|
+
* anything else could agree with the header and disagree with what the caller
|
|
63
|
+
* was given.
|
|
64
|
+
*/
|
|
65
|
+
async function carriesId(client, uid, bare) {
|
|
66
|
+
const message = await client.fetchOne(String(uid), { envelope: true }, { uid: true });
|
|
67
|
+
const actual = message && message.envelope ? (message.envelope.messageId ?? '') : '';
|
|
68
|
+
return bareForm(actual.trim()) === bare;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Searches one mailbox for a Message-ID and returns the UID if present.
|
|
72
|
+
*
|
|
73
|
+
* Two attempts, and the second one exists because of a real message.
|
|
74
|
+
*
|
|
75
|
+
* RFC 5322 requires the angle brackets, so `normaliseMessageId` puts them back
|
|
76
|
+
* on and the first search asks for the bracketed form. That search is exact in
|
|
77
|
+
* practice even though the comparison is a substring one: `<abc>` cannot occur
|
|
78
|
+
* inside `<xyzabc>`, because the bracket has to sit immediately before the
|
|
79
|
+
* identifier.
|
|
80
|
+
*
|
|
81
|
+
* Real mail does not always obey. A message was found whose header carries the
|
|
82
|
+
* identifier bare, and the envelope hands it out exactly as it stands, so
|
|
83
|
+
* everything this server had already reported for that message was unfindable:
|
|
84
|
+
* the bracketed search never matched, and it was the only search there was.
|
|
85
|
+
* Marking 67 messages as read left that one behind with no way to reach it.
|
|
86
|
+
*
|
|
87
|
+
* The second attempt therefore drops the brackets, which finds both spellings,
|
|
88
|
+
* and every hit is verified because that search can also match a message that
|
|
89
|
+
* merely contains the identifier. It only runs when the first one came back
|
|
90
|
+
* empty, so nothing normal pays for it.
|
|
91
|
+
*/
|
|
92
|
+
export async function findByMessageId(client, messageId) {
|
|
93
|
+
const exact = await client.search({ header: { 'message-id': messageId } }, { uid: true });
|
|
94
|
+
if (exact && exact.length > 0) {
|
|
95
|
+
// Several hits mean duplicates of the same message inside one mailbox.
|
|
96
|
+
// The highest UID is the most recent copy.
|
|
97
|
+
return Math.max(...exact);
|
|
98
|
+
}
|
|
99
|
+
const bare = bareForm(messageId);
|
|
100
|
+
const loose = await client.search({ header: { 'message-id': bare } }, { uid: true });
|
|
101
|
+
if (!loose || loose.length === 0)
|
|
102
|
+
return undefined;
|
|
103
|
+
// Newest first, so a duplicate resolves to the more recent copy like above.
|
|
104
|
+
for (const uid of [...loose].sort((a, b) => b - a)) {
|
|
105
|
+
if (await carriesId(client, uid, bare))
|
|
106
|
+
return uid;
|
|
107
|
+
}
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Resolves a Message-ID to a mailbox and UID.
|
|
112
|
+
*
|
|
113
|
+
* `hint` is a mailbox to try first. It is a shortcut, not a requirement: if the
|
|
114
|
+
* message is not there, the search falls back to "All Mail", which holds
|
|
115
|
+
* everything. That is what makes the identifier survive a move.
|
|
116
|
+
*/
|
|
117
|
+
export async function resolveMessageId(connection, rawId, hint) {
|
|
118
|
+
const messageId = normaliseMessageId(rawId);
|
|
119
|
+
const candidates = hint && hint !== ALL_MAIL ? [hint, ALL_MAIL] : [ALL_MAIL];
|
|
120
|
+
for (const path of candidates) {
|
|
121
|
+
const found = await connection.withMailbox(path, async (client, status) => {
|
|
122
|
+
if (status.messages === 0)
|
|
123
|
+
return undefined;
|
|
124
|
+
const uid = await findByMessageId(client, messageId);
|
|
125
|
+
return uid === undefined ? undefined : { uid, uidValidity: status.uidValidity };
|
|
126
|
+
});
|
|
127
|
+
if (found) {
|
|
128
|
+
return { messageId, path, uid: found.uid, uidValidity: found.uidValidity };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
throw new BridgeError(`No message with the id ${messageId} was found${hint ? ` in "${hint}" or in "${ALL_MAIL}"` : ` in "${ALL_MAIL}"`}. ` +
|
|
132
|
+
'The message may have been deleted, or the id may be incomplete. ' +
|
|
133
|
+
'Identifiers come from list_messages or search_messages and have to be passed on unchanged.');
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=ids.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ids.js","sourceRoot":"","sources":["../../src/mail/ids.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AAEjD,mFAAmF;AACnF,MAAM,CAAC,MAAM,QAAQ,GAAG,UAAU,CAAA;AAElC;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAC,EAAU;IAC3C,MAAM,OAAO,GAAG,EAAE,CAAC,IAAI,EAAE,CAAA;IACzB,IAAI,CAAC,OAAO;QAAE,MAAM,IAAI,WAAW,CAAC,yCAAyC,CAAC,CAAA;IAC9E,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAA;IAC1C,OAAO,IAAI,IAAI,GAAG,CAAA;AACpB,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAW;IAC1C,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAA;IAC1B,OAAO,OAAO,CAAC,CAAC,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;AACnD,CAAC;AAYD,4EAA4E;AAC5E,SAAS,QAAQ,CAAC,SAAiB;IACjC,OAAO,SAAS,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAA;AACxC,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,KAAK,UAAU,SAAS,CAAC,MAAgB,EAAE,GAAW,EAAE,IAAY;IAClE,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAA;IACrF,MAAM,MAAM,GAAG,OAAO,IAAI,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IACpF,OAAO,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAA;AACzC,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,MAAgB,EAChB,SAAiB;IAEjB,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,EAAE,YAAY,EAAE,SAAS,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAA;IACzF,IAAI,KAAK,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC9B,uEAAuE;QACvE,2CAA2C;QAC3C,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,CAAA;IAC3B,CAAC;IAED,MAAM,IAAI,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAA;IAChC,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAA;IACpF,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAA;IAElD,4EAA4E;IAC5E,KAAK,MAAM,GAAG,IAAI,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;QACnD,IAAI,MAAM,SAAS,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,CAAC;YAAE,OAAO,GAAG,CAAA;IACpD,CAAC;IACD,OAAO,SAAS,CAAA;AAClB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,UAAsB,EACtB,KAAa,EACb,IAAa;IAEb,MAAM,SAAS,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAA;IAE3C,MAAM,UAAU,GAAG,IAAI,IAAI,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAA;IAE5E,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;QAC9B,MAAM,KAAK,GAAG,MAAM,UAAU,CAAC,WAAW,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE;YACxE,IAAI,MAAM,CAAC,QAAQ,KAAK,CAAC;gBAAE,OAAO,SAAS,CAAA;YAC3C,MAAM,GAAG,GAAG,MAAM,eAAe,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;YACpD,OAAO,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,WAAW,EAAE,MAAM,CAAC,WAAW,EAAE,CAAA;QACjF,CAAC,CAAC,CAAA;QACF,IAAI,KAAK,EAAE,CAAC;YACV,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,WAAW,EAAE,KAAK,CAAC,WAAW,EAAE,CAAA;QAC5E,CAAC;IACH,CAAC;IAED,MAAM,IAAI,WAAW,CACnB,0BAA0B,SAAS,aAAa,IAAI,CAAC,CAAC,CAAC,QAAQ,IAAI,YAAY,QAAQ,GAAG,CAAC,CAAC,CAAC,QAAQ,QAAQ,GAAG,IAAI;QAClH,kEAAkE;QAClE,4FAA4F,CAC/F,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Applying and removing labels.
|
|
3
|
+
*
|
|
4
|
+
* The first thing in this server that writes to a mailbox. Everything here
|
|
5
|
+
* follows from four measurements against the Bridge:
|
|
6
|
+
*
|
|
7
|
+
* 1. `COPY` into a mailbox under `Labels/` applies the label. The message stays
|
|
8
|
+
* in its folder and no second message appears: "All Mail" holds every message
|
|
9
|
+
* exactly once and its count did not change.
|
|
10
|
+
* 2. The same `COPY` into a mailbox under `Folders/` **moves** the message
|
|
11
|
+
* instead. One command, two meanings, told apart by the prefix alone.
|
|
12
|
+
* 3. A label is removed with `\Deleted` plus `EXPUNGE` inside the label mailbox.
|
|
13
|
+
* The message keeps its folder, keeps its other labels and does not go to
|
|
14
|
+
* trash.
|
|
15
|
+
* 4. **A copy into a mailbox that does not exist reports success and does
|
|
16
|
+
* nothing.** No error, no TRYCREATE, and the destination is not created
|
|
17
|
+
* either. There is nothing to react to afterwards, so the label has to be
|
|
18
|
+
* verified to exist before the call, not after it.
|
|
19
|
+
*
|
|
20
|
+
* Point 2 and point 3 together are why this module refuses to touch anything
|
|
21
|
+
* outside `Labels/`. Removing a label is an EXPUNGE, and an EXPUNGE aimed at a
|
|
22
|
+
* folder deletes mail. That guard is the most important line in this file.
|
|
23
|
+
*/
|
|
24
|
+
import type { Connection } from '../bridge/connection.js';
|
|
25
|
+
/** The prefix Proton exposes labels under. Anything else is not a label. */
|
|
26
|
+
export declare const LABEL_PREFIX = "Labels/";
|
|
27
|
+
/**
|
|
28
|
+
* Brings a label name into its full path.
|
|
29
|
+
*
|
|
30
|
+
* Callers say "Test1" or "Labels/Test1" and mean the same thing. What they must
|
|
31
|
+
* not be able to say is a path that leaves the label namespace, so this rejects
|
|
32
|
+
* rather than repairs anything else.
|
|
33
|
+
*/
|
|
34
|
+
export declare function labelPath(name: string): string;
|
|
35
|
+
/** The labels that exist, as full paths. */
|
|
36
|
+
export declare function listLabels(connection: Connection): Promise<string[]>;
|
|
37
|
+
export interface LabelChange {
|
|
38
|
+
messageId: string;
|
|
39
|
+
label: string;
|
|
40
|
+
/** The mailbox the message was found in, for the answer. */
|
|
41
|
+
path: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Applies a label to a message.
|
|
45
|
+
*
|
|
46
|
+
* The result is deliberately not verified afterwards. Measured: right after a
|
|
47
|
+
* write the Bridge reports an intermediate state, and a check run immediately
|
|
48
|
+
* describes something that is not the outcome.
|
|
49
|
+
*/
|
|
50
|
+
export declare function addLabel(connection: Connection, readOnly: boolean, messageId: string, label: string, hint?: string): Promise<LabelChange>;
|
|
51
|
+
/**
|
|
52
|
+
* Removes a label from a message.
|
|
53
|
+
*
|
|
54
|
+
* The message is looked up **inside the label mailbox**, not through the
|
|
55
|
+
* ordinary resolver. A label is a mailbox of its own with its own uids, so a uid
|
|
56
|
+
* from anywhere else would address a different message here, and this operation
|
|
57
|
+
* expunges what it addresses.
|
|
58
|
+
*/
|
|
59
|
+
export declare function removeLabel(connection: Connection, readOnly: boolean, messageId: string, label: string): Promise<LabelChange & {
|
|
60
|
+
wasApplied: boolean;
|
|
61
|
+
}>;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Applying and removing labels.
|
|
3
|
+
*
|
|
4
|
+
* The first thing in this server that writes to a mailbox. Everything here
|
|
5
|
+
* follows from four measurements against the Bridge:
|
|
6
|
+
*
|
|
7
|
+
* 1. `COPY` into a mailbox under `Labels/` applies the label. The message stays
|
|
8
|
+
* in its folder and no second message appears: "All Mail" holds every message
|
|
9
|
+
* exactly once and its count did not change.
|
|
10
|
+
* 2. The same `COPY` into a mailbox under `Folders/` **moves** the message
|
|
11
|
+
* instead. One command, two meanings, told apart by the prefix alone.
|
|
12
|
+
* 3. A label is removed with `\Deleted` plus `EXPUNGE` inside the label mailbox.
|
|
13
|
+
* The message keeps its folder, keeps its other labels and does not go to
|
|
14
|
+
* trash.
|
|
15
|
+
* 4. **A copy into a mailbox that does not exist reports success and does
|
|
16
|
+
* nothing.** No error, no TRYCREATE, and the destination is not created
|
|
17
|
+
* either. There is nothing to react to afterwards, so the label has to be
|
|
18
|
+
* verified to exist before the call, not after it.
|
|
19
|
+
*
|
|
20
|
+
* Point 2 and point 3 together are why this module refuses to touch anything
|
|
21
|
+
* outside `Labels/`. Removing a label is an EXPUNGE, and an EXPUNGE aimed at a
|
|
22
|
+
* folder deletes mail. That guard is the most important line in this file.
|
|
23
|
+
*/
|
|
24
|
+
import { BridgeError } from '../bridge/errors.js';
|
|
25
|
+
import { normaliseMessageId, resolveMessageId } from './ids.js';
|
|
26
|
+
/** The prefix Proton exposes labels under. Anything else is not a label. */
|
|
27
|
+
export const LABEL_PREFIX = 'Labels/';
|
|
28
|
+
/**
|
|
29
|
+
* Brings a label name into its full path.
|
|
30
|
+
*
|
|
31
|
+
* Callers say "Test1" or "Labels/Test1" and mean the same thing. What they must
|
|
32
|
+
* not be able to say is a path that leaves the label namespace, so this rejects
|
|
33
|
+
* rather than repairs anything else.
|
|
34
|
+
*/
|
|
35
|
+
export function labelPath(name) {
|
|
36
|
+
const trimmed = name.trim();
|
|
37
|
+
if (!trimmed)
|
|
38
|
+
throw new BridgeError('An empty label name cannot be used.');
|
|
39
|
+
const full = trimmed.startsWith(LABEL_PREFIX) ? trimmed : `${LABEL_PREFIX}${trimmed}`;
|
|
40
|
+
const bare = full.slice(LABEL_PREFIX.length);
|
|
41
|
+
if (!bare) {
|
|
42
|
+
throw new BridgeError(`"${name}" names the label container itself rather than a label. Pass the label name, for example "Work".`);
|
|
43
|
+
}
|
|
44
|
+
// A nested path would let a caller aim at a folder from inside what looks
|
|
45
|
+
// like a label name. Labels have no hierarchy in Proton, so nothing is lost.
|
|
46
|
+
if (bare.includes('/')) {
|
|
47
|
+
throw new BridgeError(`"${name}" is not a label name. Labels have no sub-levels; pass a single name such as "Work".`);
|
|
48
|
+
}
|
|
49
|
+
return full;
|
|
50
|
+
}
|
|
51
|
+
/** Guard for every write in this module. Nothing outside Labels/ is touched. */
|
|
52
|
+
function assertLabelMailbox(path) {
|
|
53
|
+
if (!path.startsWith(LABEL_PREFIX) || path.slice(LABEL_PREFIX.length).includes('/')) {
|
|
54
|
+
throw new BridgeError(`Refusing to operate on "${path}". This code only ever touches mailboxes under ${LABEL_PREFIX}, ` +
|
|
55
|
+
'because removing a label expunges messages and doing that to a folder would delete mail.');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** The labels that exist, as full paths. */
|
|
59
|
+
export async function listLabels(connection) {
|
|
60
|
+
const boxes = await connection.listMailboxes();
|
|
61
|
+
return boxes.filter((b) => b.kind === 'label' && b.selectable).map((b) => b.path);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Checks that a label exists, and says what to do when it does not.
|
|
65
|
+
*
|
|
66
|
+
* Not a nicety. Measurement 4 above means a missing label produces a successful
|
|
67
|
+
* copy that changes nothing, so without this check the tool would report having
|
|
68
|
+
* applied a label that was never applied.
|
|
69
|
+
*/
|
|
70
|
+
async function requireLabel(connection, path) {
|
|
71
|
+
const labels = await listLabels(connection);
|
|
72
|
+
if (labels.includes(path))
|
|
73
|
+
return;
|
|
74
|
+
const names = labels.map((l) => l.slice(LABEL_PREFIX.length));
|
|
75
|
+
throw new BridgeError(`There is no label "${path.slice(LABEL_PREFIX.length)}". ` +
|
|
76
|
+
(names.length
|
|
77
|
+
? `The labels of this account are: ${names.join(', ')}.`
|
|
78
|
+
: 'This account has no labels yet.') +
|
|
79
|
+
' Labels have to be created in Proton itself; this server does not create them, ' +
|
|
80
|
+
'and the Bridge accepts a copy into a label that does not exist without applying anything.');
|
|
81
|
+
}
|
|
82
|
+
/** Refuses every write while the server runs read-only. */
|
|
83
|
+
function assertWritable(readOnly, what) {
|
|
84
|
+
if (readOnly) {
|
|
85
|
+
throw new BridgeError(`The server is running read-only, so ${what} is refused. ` +
|
|
86
|
+
'This is set by PROTON_MCP_READ_ONLY and the web interface reports it as the current mode.');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Applies a label to a message.
|
|
91
|
+
*
|
|
92
|
+
* The result is deliberately not verified afterwards. Measured: right after a
|
|
93
|
+
* write the Bridge reports an intermediate state, and a check run immediately
|
|
94
|
+
* describes something that is not the outcome.
|
|
95
|
+
*/
|
|
96
|
+
export async function addLabel(connection, readOnly, messageId, label, hint) {
|
|
97
|
+
assertWritable(readOnly, 'applying a label');
|
|
98
|
+
const path = labelPath(label);
|
|
99
|
+
assertLabelMailbox(path);
|
|
100
|
+
await requireLabel(connection, path);
|
|
101
|
+
const id = normaliseMessageId(messageId);
|
|
102
|
+
const resolved = await resolveMessageId(connection, id, hint);
|
|
103
|
+
await connection.withMailbox(resolved.path, async (client) => {
|
|
104
|
+
await client.messageCopy(String(resolved.uid), path, { uid: true });
|
|
105
|
+
});
|
|
106
|
+
return { messageId: resolved.messageId, label: path, path: resolved.path };
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Removes a label from a message.
|
|
110
|
+
*
|
|
111
|
+
* The message is looked up **inside the label mailbox**, not through the
|
|
112
|
+
* ordinary resolver. A label is a mailbox of its own with its own uids, so a uid
|
|
113
|
+
* from anywhere else would address a different message here, and this operation
|
|
114
|
+
* expunges what it addresses.
|
|
115
|
+
*/
|
|
116
|
+
export async function removeLabel(connection, readOnly, messageId, label) {
|
|
117
|
+
assertWritable(readOnly, 'removing a label');
|
|
118
|
+
const path = labelPath(label);
|
|
119
|
+
assertLabelMailbox(path);
|
|
120
|
+
await requireLabel(connection, path);
|
|
121
|
+
const id = normaliseMessageId(messageId);
|
|
122
|
+
const wasApplied = await connection.withMailbox(path, async (client, status) => {
|
|
123
|
+
// The Bridge answers FETCH on an empty mailbox with BAD, and a search there
|
|
124
|
+
// is pointless anyway.
|
|
125
|
+
if (status.messages === 0)
|
|
126
|
+
return false;
|
|
127
|
+
const hits = await client.search({ header: { 'message-id': id } }, { uid: true });
|
|
128
|
+
if (!Array.isArray(hits) || hits.length === 0)
|
|
129
|
+
return false;
|
|
130
|
+
// Guarded again, immediately before the expunge. The path cannot have
|
|
131
|
+
// changed between the check above and here, but this is the line that
|
|
132
|
+
// deletes, and it should not depend on a check twenty lines away.
|
|
133
|
+
assertLabelMailbox(status.path);
|
|
134
|
+
await client.messageDelete(String(Math.max(...hits)), { uid: true });
|
|
135
|
+
return true;
|
|
136
|
+
});
|
|
137
|
+
return { messageId: id, label: path, path, wasApplied };
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=labels.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"labels.js","sourceRoot":"","sources":["../../src/mail/labels.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,91 @@
|
|
|
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
|
+
/**
|
|
45
|
+
* The elements a message may use.
|
|
46
|
+
*
|
|
47
|
+
* Absent by design and worth naming, because the reason is the measurement
|
|
48
|
+
* above rather than taste: `style`, `script`, `noscript`, `textarea`, `head`,
|
|
49
|
+
* `title`, `meta`, `link`, `iframe`, `object`, `form`, `input`, `button`.
|
|
50
|
+
*/
|
|
51
|
+
export declare const PERMITTED_TAGS: Set<string>;
|
|
52
|
+
/** Style properties a message may set. */
|
|
53
|
+
export declare const PERMITTED_STYLES: Set<string>;
|
|
54
|
+
/** One reason a message was refused. */
|
|
55
|
+
export interface MarkupProblem {
|
|
56
|
+
/** What was found, in the caller's own markup. */
|
|
57
|
+
found: string;
|
|
58
|
+
/** Why it cannot be sent, and what to do instead. */
|
|
59
|
+
reason: string;
|
|
60
|
+
}
|
|
61
|
+
/** An address the message points at, which a person has to see before sending. */
|
|
62
|
+
export interface MarkupUrl {
|
|
63
|
+
kind: 'link' | 'image';
|
|
64
|
+
url: string;
|
|
65
|
+
/** The text a reader sees for a link, or the alt text of an image. */
|
|
66
|
+
label: string;
|
|
67
|
+
}
|
|
68
|
+
export interface MarkupReading {
|
|
69
|
+
problems: MarkupProblem[];
|
|
70
|
+
urls: MarkupUrl[];
|
|
71
|
+
/** Text a recipient can read that the text preview leaves out. */
|
|
72
|
+
hiddenText: string[];
|
|
73
|
+
/** Content ids an image refers to, which have to be provided as parts. */
|
|
74
|
+
contentIds: string[];
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Reads a message's markup: what is wrong with it, and what has to be shown.
|
|
78
|
+
*
|
|
79
|
+
* One pass, because the two questions are the same walk over the document and
|
|
80
|
+
* answering them separately is how they drift apart.
|
|
81
|
+
*/
|
|
82
|
+
export declare function readMarkup(html: string): MarkupReading;
|
|
83
|
+
/**
|
|
84
|
+
* Refuses markup this server will not send.
|
|
85
|
+
*
|
|
86
|
+
* Refused rather than repaired, deliberately. Stripping what is not permitted
|
|
87
|
+
* would mean the message that goes out is not the message that was written, and
|
|
88
|
+
* nobody would find out: not the caller, whose formatting quietly vanishes, and
|
|
89
|
+
* not the person confirming, who has nothing to compare against.
|
|
90
|
+
*/
|
|
91
|
+
export declare function assertSendableMarkup(html: string): MarkupReading;
|