@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,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pinning the Bridge's self-signed certificate.
|
|
3
|
+
*
|
|
4
|
+
* The problem: the Bridge generates its own certificate and keeps it inside its
|
|
5
|
+
* encrypted vault rather than on disk, so it cannot be trusted in advance. The
|
|
6
|
+
* lazy answer is to switch verification off and never look again, which means
|
|
7
|
+
* anything that manages to listen on the port gets the Bridge password.
|
|
8
|
+
*
|
|
9
|
+
* The answer here is trust on first use. On the first connection the certificate
|
|
10
|
+
* is fetched and its fingerprint recorded. From then on it is pinned: Node
|
|
11
|
+
* verifies against that one certificate and nothing else. A changed certificate
|
|
12
|
+
* stops the server with an explanation instead of being waved through.
|
|
13
|
+
*
|
|
14
|
+
* Fetching needs a STARTTLS handshake of our own, because imapflow does not hand
|
|
15
|
+
* the certificate out. Verified against Bridge 03.25.00: checkServerIdentity is
|
|
16
|
+
* never invoked while verification is off, and the certificate is not attached
|
|
17
|
+
* to the verification error either.
|
|
18
|
+
*/
|
|
19
|
+
import { connect as netConnect } from 'node:net';
|
|
20
|
+
import { connect as tlsConnect } from 'node:tls';
|
|
21
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
22
|
+
import { dirname, join } from 'node:path';
|
|
23
|
+
import { homedir } from 'node:os';
|
|
24
|
+
import { BridgeError } from './errors.js';
|
|
25
|
+
/** Where the pinned certificate is stored. Next to the credentials file. */
|
|
26
|
+
export function pinPath(env = process.env) {
|
|
27
|
+
if (env.PROTON_MCP_CERT_FILE)
|
|
28
|
+
return env.PROTON_MCP_CERT_FILE;
|
|
29
|
+
return join(homedir(), '.config', 'proton-mcp', 'known-certificate.json');
|
|
30
|
+
}
|
|
31
|
+
/** Turns a DER certificate into the PEM form Node expects as a CA. */
|
|
32
|
+
export function toPem(der) {
|
|
33
|
+
const base64 = der.toString('base64');
|
|
34
|
+
const lines = base64.match(/.{1,64}/g) ?? [];
|
|
35
|
+
return `-----BEGIN CERTIFICATE-----\n${lines.join('\n')}\n-----END CERTIFICATE-----\n`;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Fetches the certificate by doing the STARTTLS dance manually.
|
|
39
|
+
*
|
|
40
|
+
* Deliberately minimal: it reads the greeting, sends STARTTLS, waits for the OK
|
|
41
|
+
* and upgrades. It does not log in, so no credentials are involved at this
|
|
42
|
+
* point.
|
|
43
|
+
*/
|
|
44
|
+
export function fetchCertificate(host, port, timeoutMs = 10_000) {
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
const socket = netConnect({ host, port });
|
|
47
|
+
let settled = false;
|
|
48
|
+
let stage = 'greeting';
|
|
49
|
+
let buffer = '';
|
|
50
|
+
const finish = (error, value) => {
|
|
51
|
+
if (settled)
|
|
52
|
+
return;
|
|
53
|
+
settled = true;
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
socket.destroy();
|
|
56
|
+
if (error)
|
|
57
|
+
reject(error);
|
|
58
|
+
else if (value)
|
|
59
|
+
resolve(value);
|
|
60
|
+
};
|
|
61
|
+
const timer = setTimeout(() => {
|
|
62
|
+
finish(new BridgeError(`The Bridge at ${host}:${port} did not answer within ${timeoutMs} ms while reading its certificate. ` +
|
|
63
|
+
'Check that it is running and unlocked.'));
|
|
64
|
+
}, timeoutMs);
|
|
65
|
+
socket.on('error', (error) => finish(error));
|
|
66
|
+
socket.setEncoding('utf8');
|
|
67
|
+
socket.on('data', (chunk) => {
|
|
68
|
+
buffer += chunk;
|
|
69
|
+
if (stage === 'greeting') {
|
|
70
|
+
// The greeting is a single untagged line.
|
|
71
|
+
if (!buffer.includes('\r\n'))
|
|
72
|
+
return;
|
|
73
|
+
if (!/^\* (OK|PREAUTH)/i.test(buffer)) {
|
|
74
|
+
finish(new BridgeError(`${host}:${port} did not answer like an IMAP server. Received: ${buffer.slice(0, 80).trim()}`));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
buffer = '';
|
|
78
|
+
stage = 'starttls';
|
|
79
|
+
socket.write('A1 STARTTLS\r\n');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (!/^A1 /m.test(buffer))
|
|
83
|
+
return;
|
|
84
|
+
if (!/^A1 OK/im.test(buffer)) {
|
|
85
|
+
finish(new BridgeError(`${host}:${port} refused STARTTLS. Received: ${buffer.slice(0, 80).trim()}. ` +
|
|
86
|
+
'The Bridge normally requires STARTTLS, so this port may belong to something else.'));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
// Encoding was set for the plaintext part, and it has to go before the
|
|
90
|
+
// socket is handed to the TLS layer.
|
|
91
|
+
socket.setEncoding(null);
|
|
92
|
+
const secure = tlsConnect({
|
|
93
|
+
socket,
|
|
94
|
+
// The point of this connection is to look at the certificate, so it
|
|
95
|
+
// cannot be validated beforehand. Nothing is sent over it.
|
|
96
|
+
rejectUnauthorized: false,
|
|
97
|
+
}, () => {
|
|
98
|
+
const cert = secure.getPeerCertificate();
|
|
99
|
+
if (!cert || !cert.raw) {
|
|
100
|
+
finish(new BridgeError(`${host}:${port} presented no certificate.`));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// A certificate may carry several common names, in which case Node
|
|
104
|
+
// hands over an array.
|
|
105
|
+
const cn = cert.subject?.CN;
|
|
106
|
+
const value = {
|
|
107
|
+
fingerprint256: cert.fingerprint256,
|
|
108
|
+
pem: toPem(cert.raw),
|
|
109
|
+
subject: Array.isArray(cn) ? cn.join(', ') : (cn ?? JSON.stringify(cert.subject ?? {})),
|
|
110
|
+
validTo: cert.valid_to ?? '',
|
|
111
|
+
};
|
|
112
|
+
secure.destroy();
|
|
113
|
+
finish(undefined, value);
|
|
114
|
+
});
|
|
115
|
+
secure.on('error', (error) => finish(error));
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
/** Reads the pinned certificate. Absence means nothing has been pinned yet. */
|
|
120
|
+
export function loadPinned(path) {
|
|
121
|
+
try {
|
|
122
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
123
|
+
if (!parsed.fingerprint256 || !parsed.pem)
|
|
124
|
+
return undefined;
|
|
125
|
+
return parsed;
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/** Records a certificate as trusted. */
|
|
132
|
+
export function savePinned(path, entry) {
|
|
133
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
134
|
+
writeFileSync(path, `${JSON.stringify(entry, null, 2)}\n`, { mode: 0o600 });
|
|
135
|
+
}
|
|
136
|
+
/** The message shown when the certificate no longer matches. */
|
|
137
|
+
export function mismatchMessage(pinned, actual, path) {
|
|
138
|
+
return (`The certificate of the Bridge at ${pinned.host}:${pinned.port} has changed. Refusing to connect.\n\n` +
|
|
139
|
+
` expected: ${pinned.fingerprint256}\n` +
|
|
140
|
+
` received: ${actual.fingerprint256}\n\n` +
|
|
141
|
+
`The expected one was recorded on ${pinned.firstSeen.slice(0, 10)}.\n\n` +
|
|
142
|
+
'If you reinstalled Proton Mail Bridge or moved to a different machine, this is expected. ' +
|
|
143
|
+
`Delete ${path} and the next connection will record the new certificate.\n\n` +
|
|
144
|
+
'If you did not change anything, do not delete it. Something else may be listening on that port, ' +
|
|
145
|
+
'and it would receive your Bridge password.');
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Makes sure the certificate is known, and returns it for use as a CA.
|
|
149
|
+
*
|
|
150
|
+
* On first use it records what it finds, which is the only moment this is
|
|
151
|
+
* open to a machine-in-the-middle. Every later connection is pinned.
|
|
152
|
+
*/
|
|
153
|
+
export async function ensurePinned(host, port, options = {}) {
|
|
154
|
+
const path = options.path ?? pinPath();
|
|
155
|
+
const pinned = loadPinned(path);
|
|
156
|
+
const actual = await (options.fetch ?? fetchCertificate)(host, port);
|
|
157
|
+
if (!pinned || pinned.host !== host || pinned.port !== port) {
|
|
158
|
+
const entry = {
|
|
159
|
+
host,
|
|
160
|
+
port,
|
|
161
|
+
fingerprint256: actual.fingerprint256,
|
|
162
|
+
pem: actual.pem,
|
|
163
|
+
subject: actual.subject,
|
|
164
|
+
validTo: actual.validTo,
|
|
165
|
+
firstSeen: (options.now?.() ?? new Date()).toISOString(),
|
|
166
|
+
};
|
|
167
|
+
savePinned(path, entry);
|
|
168
|
+
options.onFirstUse?.(entry);
|
|
169
|
+
return entry;
|
|
170
|
+
}
|
|
171
|
+
if (pinned.fingerprint256 !== actual.fingerprint256) {
|
|
172
|
+
throw new BridgeError(mismatchMessage(pinned, actual, path));
|
|
173
|
+
}
|
|
174
|
+
return pinned;
|
|
175
|
+
}
|
|
176
|
+
//# sourceMappingURL=certificate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"certificate.js","sourceRoot":"","sources":["../../src/bridge/certificate.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connection to the Bridge's IMAP interface.
|
|
3
|
+
*
|
|
4
|
+
* One connection is held for the whole process rather than opened per call:
|
|
5
|
+
* the Bridge dislikes connection churn and the handshake costs time. If the
|
|
6
|
+
* Bridge was restarted or the socket died in between, the next call rebuilds
|
|
7
|
+
* it transparently.
|
|
8
|
+
*/
|
|
9
|
+
import { ImapFlow } from 'imapflow';
|
|
10
|
+
import type { Config, BridgeCredentials } from '../config.js';
|
|
11
|
+
import { BridgeError } from './errors.js';
|
|
12
|
+
/** Thrown when a tool is used before anyone has signed in. */
|
|
13
|
+
export declare class NotSignedInError extends BridgeError {
|
|
14
|
+
name: string;
|
|
15
|
+
}
|
|
16
|
+
/** A mailbox as reported by the Bridge. */
|
|
17
|
+
export interface Mailbox {
|
|
18
|
+
/** The full IMAP path, for example "Folders/Receipts/Invoices". */
|
|
19
|
+
path: string;
|
|
20
|
+
/** The last path segment, for example "Invoices". */
|
|
21
|
+
name: string;
|
|
22
|
+
/** Which kind of mailbox this is. Proton treats folders and labels as different things. */
|
|
23
|
+
kind: 'system' | 'folder' | 'label';
|
|
24
|
+
/** For system mailboxes, the IMAP special-use attribute, for example "\\Inbox". */
|
|
25
|
+
specialUse?: string;
|
|
26
|
+
/** Whether the mailbox can hold messages. The "Folders" and "Labels" containers cannot. */
|
|
27
|
+
selectable: boolean;
|
|
28
|
+
}
|
|
29
|
+
/** Counters for a mailbox, cheap to obtain. */
|
|
30
|
+
export interface MailboxStatus {
|
|
31
|
+
path: string;
|
|
32
|
+
messages: number;
|
|
33
|
+
unseen: number;
|
|
34
|
+
/** Changes when the Bridge renumbers UIDs, which invalidates every cached UID. */
|
|
35
|
+
uidValidity: string;
|
|
36
|
+
uidNext: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Maps an IMAP path onto what it means in Proton.
|
|
40
|
+
*
|
|
41
|
+
* Proton exposes user folders below "Folders/" and labels below "Labels/".
|
|
42
|
+
* Everything else is a system mailbox.
|
|
43
|
+
*/
|
|
44
|
+
export declare function determineKind(path: string): Mailbox['kind'];
|
|
45
|
+
/** Whether an address keeps traffic on this machine. */
|
|
46
|
+
export declare function isLocal(host: string): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Decides whether a failure is worth one retry on a fresh connection.
|
|
49
|
+
*
|
|
50
|
+
* Only transport-level problems qualify. A rejected login or a missing
|
|
51
|
+
* mailbox would fail again on a new connection, and retrying would only
|
|
52
|
+
* double the waiting time before the user sees the real reason.
|
|
53
|
+
*/
|
|
54
|
+
export declare function isTransportFailure(error: unknown): boolean;
|
|
55
|
+
export declare class Connection {
|
|
56
|
+
#private;
|
|
57
|
+
private readonly config;
|
|
58
|
+
/**
|
|
59
|
+
* Supplies the credentials, or nothing when no one has signed in.
|
|
60
|
+
*
|
|
61
|
+
* A function rather than a value: they can arrive through the web interface
|
|
62
|
+
* long after the server started, and they can be revoked while it runs.
|
|
63
|
+
*/
|
|
64
|
+
private readonly getCredentials;
|
|
65
|
+
/** Where notices go. stderr, never stdout, which carries the protocol. */
|
|
66
|
+
private readonly notify;
|
|
67
|
+
constructor(config: Config,
|
|
68
|
+
/**
|
|
69
|
+
* Supplies the credentials, or nothing when no one has signed in.
|
|
70
|
+
*
|
|
71
|
+
* A function rather than a value: they can arrive through the web interface
|
|
72
|
+
* long after the server started, and they can be revoked while it runs.
|
|
73
|
+
*/
|
|
74
|
+
getCredentials: () => Promise<BridgeCredentials | undefined>,
|
|
75
|
+
/** Where notices go. stderr, never stdout, which carries the protocol. */
|
|
76
|
+
notify?: (message: string) => void);
|
|
77
|
+
/** Opens the connection unless a usable one is already there. */
|
|
78
|
+
open(): Promise<ImapFlow>;
|
|
79
|
+
/** Lists every mailbox. */
|
|
80
|
+
listMailboxes(): Promise<Mailbox[]>;
|
|
81
|
+
/**
|
|
82
|
+
* Counters for a mailbox without selecting it.
|
|
83
|
+
*
|
|
84
|
+
* Needed before every fetch: the Bridge answers `FETCH 1:*` on an empty
|
|
85
|
+
* mailbox with `BAD no such message` instead of an empty result, so the
|
|
86
|
+
* count has to be known in advance.
|
|
87
|
+
*/
|
|
88
|
+
status(path: string): Promise<MailboxStatus>;
|
|
89
|
+
/**
|
|
90
|
+
* Opens a mailbox, runs `operation` and releases the lock afterwards.
|
|
91
|
+
*
|
|
92
|
+
* A single IMAP connection has exactly one selected mailbox, so concurrent
|
|
93
|
+
* calls would otherwise pull the rug out from under each other. imapflow
|
|
94
|
+
* serialises this via a lock, and every mailbox access goes through here.
|
|
95
|
+
*/
|
|
96
|
+
withMailbox<T>(path: string, operation: (client: ImapFlow, status: MailboxStatus) => Promise<T>): Promise<T>;
|
|
97
|
+
/** Closes the connection if one is open. */
|
|
98
|
+
close(): Promise<void>;
|
|
99
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connection to the Bridge's IMAP interface.
|
|
3
|
+
*
|
|
4
|
+
* One connection is held for the whole process rather than opened per call:
|
|
5
|
+
* the Bridge dislikes connection churn and the handshake costs time. If the
|
|
6
|
+
* Bridge was restarted or the socket died in between, the next call rebuilds
|
|
7
|
+
* it transparently.
|
|
8
|
+
*/
|
|
9
|
+
import { ImapFlow } from 'imapflow';
|
|
10
|
+
import { explainError, BridgeError } from './errors.js';
|
|
11
|
+
import { ensurePinned } from './certificate.js';
|
|
12
|
+
/** Thrown when a tool is used before anyone has signed in. */
|
|
13
|
+
export class NotSignedInError extends BridgeError {
|
|
14
|
+
name = 'NotSignedInError';
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Maps an IMAP path onto what it means in Proton.
|
|
18
|
+
*
|
|
19
|
+
* Proton exposes user folders below "Folders/" and labels below "Labels/".
|
|
20
|
+
* Everything else is a system mailbox.
|
|
21
|
+
*/
|
|
22
|
+
export function determineKind(path) {
|
|
23
|
+
if (path === 'Folders' || path.startsWith('Folders/'))
|
|
24
|
+
return 'folder';
|
|
25
|
+
if (path === 'Labels' || path.startsWith('Labels/'))
|
|
26
|
+
return 'label';
|
|
27
|
+
return 'system';
|
|
28
|
+
}
|
|
29
|
+
/** Whether an address keeps traffic on this machine. */
|
|
30
|
+
export function isLocal(host) {
|
|
31
|
+
return host === '127.0.0.1' || host === '::1' || host === 'localhost';
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Decides whether a failure is worth one retry on a fresh connection.
|
|
35
|
+
*
|
|
36
|
+
* Only transport-level problems qualify. A rejected login or a missing
|
|
37
|
+
* mailbox would fail again on a new connection, and retrying would only
|
|
38
|
+
* double the waiting time before the user sees the real reason.
|
|
39
|
+
*/
|
|
40
|
+
export function isTransportFailure(error) {
|
|
41
|
+
const e = error;
|
|
42
|
+
const code = e?.code ?? '';
|
|
43
|
+
if (['ECONNRESET', 'EPIPE', 'ETIMEDOUT', 'ECONNREFUSED', 'ERR_STREAM_DESTROYED'].includes(code)) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
return /socket|connection closed|not connected|disconnected/i.test(e?.message ?? '');
|
|
47
|
+
}
|
|
48
|
+
export class Connection {
|
|
49
|
+
config;
|
|
50
|
+
getCredentials;
|
|
51
|
+
notify;
|
|
52
|
+
#client;
|
|
53
|
+
/** Guards against several callers opening a connection at the same time. */
|
|
54
|
+
#opening;
|
|
55
|
+
constructor(config,
|
|
56
|
+
/**
|
|
57
|
+
* Supplies the credentials, or nothing when no one has signed in.
|
|
58
|
+
*
|
|
59
|
+
* A function rather than a value: they can arrive through the web interface
|
|
60
|
+
* long after the server started, and they can be revoked while it runs.
|
|
61
|
+
*/
|
|
62
|
+
getCredentials,
|
|
63
|
+
/** Where notices go. stderr, never stdout, which carries the protocol. */
|
|
64
|
+
notify = (m) => process.stderr.write(`proton-mcp: ${m}\n`)) {
|
|
65
|
+
this.config = config;
|
|
66
|
+
this.getCredentials = getCredentials;
|
|
67
|
+
this.notify = notify;
|
|
68
|
+
}
|
|
69
|
+
/** Opens the connection unless a usable one is already there. */
|
|
70
|
+
async open() {
|
|
71
|
+
if (this.#client?.usable)
|
|
72
|
+
return this.#client;
|
|
73
|
+
// A second caller waits for the first attempt instead of starting its own.
|
|
74
|
+
if (this.#opening)
|
|
75
|
+
return this.#opening;
|
|
76
|
+
this.#opening = this.#connect().finally(() => {
|
|
77
|
+
this.#opening = undefined;
|
|
78
|
+
});
|
|
79
|
+
return this.#opening;
|
|
80
|
+
}
|
|
81
|
+
async #connect() {
|
|
82
|
+
const credentials = await this.getCredentials();
|
|
83
|
+
if (!credentials) {
|
|
84
|
+
throw new NotSignedInError('No Bridge credentials are available yet, so there is nothing to connect with.');
|
|
85
|
+
}
|
|
86
|
+
// The Bridge only ever listens on the loopback interface. A different host
|
|
87
|
+
// means either a tunnel or a misconfiguration, and in the second case the
|
|
88
|
+
// Bridge password would go somewhere it does not belong.
|
|
89
|
+
if (!isLocal(this.config.host)) {
|
|
90
|
+
this.notify(`BRIDGE_HOST is "${this.config.host}", which is not this machine. ` +
|
|
91
|
+
'Proton Mail Bridge only listens on 127.0.0.1, so this only makes sense if you ' +
|
|
92
|
+
'deliberately put a tunnel in front of it. Otherwise correct BRIDGE_HOST.');
|
|
93
|
+
}
|
|
94
|
+
// Pin the certificate before handing over any credentials. On first use this
|
|
95
|
+
// records what it finds, afterwards it verifies. A changed certificate stops
|
|
96
|
+
// us here rather than after the password has gone out.
|
|
97
|
+
const pinned = await ensurePinned(this.config.host, this.config.imapPort, {
|
|
98
|
+
onFirstUse: (entry) => this.notify(`recorded the Bridge certificate on first use, fingerprint ${entry.fingerprint256}. ` +
|
|
99
|
+
'Further connections are verified against it.'),
|
|
100
|
+
});
|
|
101
|
+
const client = new ImapFlow({
|
|
102
|
+
host: this.config.host,
|
|
103
|
+
port: this.config.imapPort,
|
|
104
|
+
// The Bridge expects a plaintext connection upgraded via STARTTLS,
|
|
105
|
+
// not a direct TLS handshake.
|
|
106
|
+
secure: false,
|
|
107
|
+
auth: { user: credentials.user, pass: credentials.pass },
|
|
108
|
+
tls: {
|
|
109
|
+
// Verified against exactly one certificate: the pinned one. This is
|
|
110
|
+
// stricter than the public trust store, which would accept any
|
|
111
|
+
// certificate signed by any recognised authority.
|
|
112
|
+
rejectUnauthorized: true,
|
|
113
|
+
ca: [pinned.pem],
|
|
114
|
+
// The Bridge's certificate does not carry 127.0.0.1 as a name, so the
|
|
115
|
+
// hostname check has to be skipped. It adds nothing on top of pinning:
|
|
116
|
+
// matching a specific certificate is a stronger statement than matching
|
|
117
|
+
// a name inside it.
|
|
118
|
+
checkServerIdentity: () => undefined,
|
|
119
|
+
},
|
|
120
|
+
// No logging: the library would otherwise write headers and subject
|
|
121
|
+
// lines to stderr.
|
|
122
|
+
logger: false,
|
|
123
|
+
});
|
|
124
|
+
// Without a listener, a socket error after a successful connect would
|
|
125
|
+
// become an unhandled event and take down the process.
|
|
126
|
+
client.on('error', () => {
|
|
127
|
+
// The state is checked via client.usable before every use, so the next
|
|
128
|
+
// call rebuilds the connection. Nothing to do here beyond not crashing.
|
|
129
|
+
});
|
|
130
|
+
try {
|
|
131
|
+
await client.connect();
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
throw explainError(error, this.config.host, this.config.imapPort);
|
|
135
|
+
}
|
|
136
|
+
this.#client = client;
|
|
137
|
+
return client;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Runs an operation and retries it once on a fresh connection if the
|
|
141
|
+
* transport failed. That is the case when the Bridge was restarted between
|
|
142
|
+
* two calls, which is common: it goes down with the machine and comes back
|
|
143
|
+
* locked.
|
|
144
|
+
*/
|
|
145
|
+
async #withRetry(operation) {
|
|
146
|
+
const client = await this.open();
|
|
147
|
+
try {
|
|
148
|
+
return await operation(client);
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
if (!isTransportFailure(error)) {
|
|
152
|
+
throw error instanceof BridgeError
|
|
153
|
+
? error
|
|
154
|
+
: explainError(error, this.config.host, this.config.imapPort);
|
|
155
|
+
}
|
|
156
|
+
// Drop the dead connection and try exactly once more.
|
|
157
|
+
this.#client = undefined;
|
|
158
|
+
const fresh = await this.open();
|
|
159
|
+
try {
|
|
160
|
+
return await operation(fresh);
|
|
161
|
+
}
|
|
162
|
+
catch (again) {
|
|
163
|
+
throw explainError(again, this.config.host, this.config.imapPort);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/** Lists every mailbox. */
|
|
168
|
+
async listMailboxes() {
|
|
169
|
+
const raw = await this.#withRetry((client) => client.list());
|
|
170
|
+
return raw.map((box) => {
|
|
171
|
+
const path = box.path;
|
|
172
|
+
const segments = path.split('/');
|
|
173
|
+
const entry = {
|
|
174
|
+
path,
|
|
175
|
+
name: segments[segments.length - 1] ?? path,
|
|
176
|
+
kind: determineKind(path),
|
|
177
|
+
selectable: !box.flags.has('\\Noselect'),
|
|
178
|
+
};
|
|
179
|
+
if (box.specialUse)
|
|
180
|
+
entry.specialUse = box.specialUse;
|
|
181
|
+
return entry;
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Counters for a mailbox without selecting it.
|
|
186
|
+
*
|
|
187
|
+
* Needed before every fetch: the Bridge answers `FETCH 1:*` on an empty
|
|
188
|
+
* mailbox with `BAD no such message` instead of an empty result, so the
|
|
189
|
+
* count has to be known in advance.
|
|
190
|
+
*/
|
|
191
|
+
async status(path) {
|
|
192
|
+
const st = await this.#withRetry((client) => client.status(path, { messages: true, unseen: true, uidValidity: true, uidNext: true }));
|
|
193
|
+
return {
|
|
194
|
+
path,
|
|
195
|
+
messages: st.messages ?? 0,
|
|
196
|
+
unseen: st.unseen ?? 0,
|
|
197
|
+
// uidValidity arrives as a BigInt and is only ever compared, never
|
|
198
|
+
// calculated with, so a string is the honest representation.
|
|
199
|
+
uidValidity: String(st.uidValidity ?? ''),
|
|
200
|
+
uidNext: st.uidNext ?? 0,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Opens a mailbox, runs `operation` and releases the lock afterwards.
|
|
205
|
+
*
|
|
206
|
+
* A single IMAP connection has exactly one selected mailbox, so concurrent
|
|
207
|
+
* calls would otherwise pull the rug out from under each other. imapflow
|
|
208
|
+
* serialises this via a lock, and every mailbox access goes through here.
|
|
209
|
+
*/
|
|
210
|
+
async withMailbox(path, operation) {
|
|
211
|
+
const status = await this.status(path);
|
|
212
|
+
return this.#withRetry(async (client) => {
|
|
213
|
+
let lock;
|
|
214
|
+
try {
|
|
215
|
+
lock = await client.getMailboxLock(path);
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
throw new BridgeError(`The mailbox "${path}" could not be opened. Check the exact spelling; ` +
|
|
219
|
+
'user folders live below "Folders/" and labels below "Labels/". ' +
|
|
220
|
+
'Use list_folders to see the available paths.', 'none', { cause: error });
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
return await operation(client, status);
|
|
224
|
+
}
|
|
225
|
+
finally {
|
|
226
|
+
lock.release();
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
/** Closes the connection if one is open. */
|
|
231
|
+
async close() {
|
|
232
|
+
const client = this.#client;
|
|
233
|
+
this.#client = undefined;
|
|
234
|
+
if (!client)
|
|
235
|
+
return;
|
|
236
|
+
try {
|
|
237
|
+
await client.logout();
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
// While shutting down, a failed logout attempt does not matter.
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
//# sourceMappingURL=connection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection.js","sourceRoot":"","sources":["../../src/bridge/connection.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turns technical failures into sentences a language model can act on.
|
|
3
|
+
*
|
|
4
|
+
* For an MCP server the error message is part of the interface: it ends up in
|
|
5
|
+
* the model's context, which then has to work out what to do next.
|
|
6
|
+
* "ECONNREFUSED 127.0.0.1:1143" does not help with that.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* What would fix it, as a category rather than as a sentence.
|
|
10
|
+
*
|
|
11
|
+
* The sentence needs the address of the configuration page, and this module has
|
|
12
|
+
* no business knowing that. So the diagnosis happens here, where the error codes
|
|
13
|
+
* are, and the remedy is written where the address is known. See describeFailure.
|
|
14
|
+
*/
|
|
15
|
+
export type BridgeRemedy =
|
|
16
|
+
/** Nothing is listening. The Bridge is not running, or the port is wrong. */
|
|
17
|
+
'unreachable'
|
|
18
|
+
/** It answered and refused the login. The stored password is stale. */
|
|
19
|
+
| 'credentials'
|
|
20
|
+
/** It is there but not ready: starting up, or locked. */
|
|
21
|
+
| 'not-ready'
|
|
22
|
+
/** Nothing the user can do from the interface. */
|
|
23
|
+
| 'none';
|
|
24
|
+
export declare class BridgeError extends Error {
|
|
25
|
+
name: string;
|
|
26
|
+
readonly remedy: BridgeRemedy;
|
|
27
|
+
constructor(message: string, remedy?: BridgeRemedy, options?: ErrorOptions);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Builds an understandable message from an arbitrary failure.
|
|
31
|
+
*
|
|
32
|
+
* `host` and `port` appear in the text because the ports are configurable and
|
|
33
|
+
* a wrong port is the second most common cause.
|
|
34
|
+
*/
|
|
35
|
+
export declare function explainError(error: unknown, host: string, port: number): BridgeError;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turns technical failures into sentences a language model can act on.
|
|
3
|
+
*
|
|
4
|
+
* For an MCP server the error message is part of the interface: it ends up in
|
|
5
|
+
* the model's context, which then has to work out what to do next.
|
|
6
|
+
* "ECONNREFUSED 127.0.0.1:1143" does not help with that.
|
|
7
|
+
*/
|
|
8
|
+
export class BridgeError extends Error {
|
|
9
|
+
name = 'BridgeError';
|
|
10
|
+
remedy;
|
|
11
|
+
constructor(message, remedy = 'none', options) {
|
|
12
|
+
super(message, options);
|
|
13
|
+
this.remedy = remedy;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Builds an understandable message from an arbitrary failure.
|
|
18
|
+
*
|
|
19
|
+
* `host` and `port` appear in the text because the ports are configurable and
|
|
20
|
+
* a wrong port is the second most common cause.
|
|
21
|
+
*/
|
|
22
|
+
export function explainError(error, host, port) {
|
|
23
|
+
const e = error;
|
|
24
|
+
const address = `${host}:${port}`;
|
|
25
|
+
if (e?.code === 'ECONNREFUSED') {
|
|
26
|
+
return new BridgeError(`Nothing is accepting connections on ${address}. Proton Mail Bridge is most likely not ` +
|
|
27
|
+
'running: it is a desktop application that has to be started and unlocked by the person ' +
|
|
28
|
+
'using it, and this server can neither start it nor unlock it. If it is running, it may ' +
|
|
29
|
+
'be listening on a different port, which the Bridge shows under Settings, Advanced.', 'unreachable', { cause: error });
|
|
30
|
+
}
|
|
31
|
+
if (e?.code === 'ETIMEDOUT' || e?.code === 'ECONNRESET') {
|
|
32
|
+
return new BridgeError(`The connection to ${address} did not come up, or it was dropped. That happens while the ` +
|
|
33
|
+
'Bridge is starting up and while it is locked, both of which pass on their own once the ' +
|
|
34
|
+
'person at the machine has unlocked it.', 'not-ready', { cause: error });
|
|
35
|
+
}
|
|
36
|
+
if (e?.code === 'ENOTFOUND' || e?.code === 'EAI_AGAIN') {
|
|
37
|
+
return new BridgeError(`The address "${host}" could not be resolved. Unless the Bridge was deliberately put on ` +
|
|
38
|
+
'another machine, BRIDGE_HOST should be 127.0.0.1.', 'unreachable', { cause: error });
|
|
39
|
+
}
|
|
40
|
+
// imapflow reports rejected logins as AUTHENTICATIONFAILED.
|
|
41
|
+
const text = `${e?.responseText ?? ''} ${e?.message ?? ''}`;
|
|
42
|
+
if (/AUTHENTICATIONFAILED|Invalid credentials|LOGIN failed/i.test(text)) {
|
|
43
|
+
return new BridgeError('The Bridge answered and refused the login, so it is running and the stored password is no ' +
|
|
44
|
+
'longer the right one. The Bridge generates a new password whenever an account is removed ' +
|
|
45
|
+
'and added again, and it can be regenerated at any time, so a password that worked ' +
|
|
46
|
+
'yesterday can be wrong today. It is not the Proton account password: the current one is ' +
|
|
47
|
+
'shown in the Bridge application under the account.', 'credentials', { cause: error });
|
|
48
|
+
}
|
|
49
|
+
if (/certificate|self.signed|DEPTH_ZERO/i.test(text)) {
|
|
50
|
+
return new BridgeError('The Bridge certificate was rejected. The Bridge uses a self-signed certificate, ' +
|
|
51
|
+
'which is only accepted for connections to 127.0.0.1.', 'none', { cause: error });
|
|
52
|
+
}
|
|
53
|
+
const raw = e?.message ?? String(error);
|
|
54
|
+
return new BridgeError(`The Bridge reported an error: ${raw}`, 'none', { cause: error });
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/bridge/errors.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAmBH,MAAM,OAAO,WAAY,SAAQ,KAAK;IAC3B,IAAI,GAAG,aAAa,CAAA;IACpB,MAAM,CAAc;IAE7B,YAAY,OAAe,EAAE,MAAM,GAAiB,MAAM,EAAE,OAAsB;QAChF,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;QACvB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;IACtB,CAAC;CACF;AAQD;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,KAAc,EAAE,IAAY,EAAE,IAAY;IACrE,MAAM,CAAC,GAAG,KAAsB,CAAA;IAChC,MAAM,OAAO,GAAG,GAAG,IAAI,IAAI,IAAI,EAAE,CAAA;IAEjC,IAAI,CAAC,EAAE,IAAI,KAAK,cAAc,EAAE,CAAC;QAC/B,OAAO,IAAI,WAAW,CACpB,uCAAuC,OAAO,0CAA0C;YACtF,yFAAyF;YACzF,yFAAyF;YACzF,oFAAoF,EACtF,aAAa,EACb,EAAE,KAAK,EAAE,KAAK,EAAE,CACjB,CAAA;IACH,CAAC;IAED,IAAI,CAAC,EAAE,IAAI,KAAK,WAAW,IAAI,CAAC,EAAE,IAAI,KAAK,YAAY,EAAE,CAAC;QACxD,OAAO,IAAI,WAAW,CACpB,qBAAqB,OAAO,8DAA8D;YACxF,yFAAyF;YACzF,wCAAwC,EAC1C,WAAW,EACX,EAAE,KAAK,EAAE,KAAK,EAAE,CACjB,CAAA;IACH,CAAC;IAED,IAAI,CAAC,EAAE,IAAI,KAAK,WAAW,IAAI,CAAC,EAAE,IAAI,KAAK,WAAW,EAAE,CAAC;QACvD,OAAO,IAAI,WAAW,CACpB,gBAAgB,IAAI,qEAAqE;YACvF,mDAAmD,EACrD,aAAa,EACb,EAAE,KAAK,EAAE,KAAK,EAAE,CACjB,CAAA;IACH,CAAC;IAED,4DAA4D;IAC5D,MAAM,IAAI,GAAG,GAAG,CAAC,EAAE,YAAY,IAAI,EAAE,IAAI,CAAC,EAAE,OAAO,IAAI,EAAE,EAAE,CAAA;IAC3D,IAAI,wDAAwD,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACxE,OAAO,IAAI,WAAW,CACpB,4FAA4F;YAC1F,2FAA2F;YAC3F,oFAAoF;YACpF,0FAA0F;YAC1F,oDAAoD,EACtD,aAAa,EACb,EAAE,KAAK,EAAE,KAAK,EAAE,CACjB,CAAA;IACH,CAAC;IAED,IAAI,qCAAqC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACrD,OAAO,IAAI,WAAW,CACpB,kFAAkF;YAChF,sDAAsD,EACxD,MAAM,EACN,EAAE,KAAK,EAAE,KAAK,EAAE,CACjB,CAAA;IACH,CAAC;IAED,MAAM,GAAG,GAAG,CAAC,EAAE,OAAO,IAAI,MAAM,CAAC,KAAK,CAAC,CAAA;IACvC,OAAO,IAAI,WAAW,CAAC,iCAAiC,GAAG,EAAE,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAA;AAC1F,CAAC"}
|