@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,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pages that come before anybody is signed in.
|
|
3
|
+
*
|
|
4
|
+
* Rendered as strings rather than with a template engine: it keeps the
|
|
5
|
+
* dependency count at zero for something that handles credentials, and there are
|
|
6
|
+
* only a handful of pages.
|
|
7
|
+
*
|
|
8
|
+
* Everything that could come from outside goes through escapeHtml. The CSP would
|
|
9
|
+
* already stop injected script from doing anything, but not from making a mess of
|
|
10
|
+
* the page, and relying on a single layer is how holes happen.
|
|
11
|
+
*
|
|
12
|
+
* These three carry no navigation. There is nothing to navigate to yet, and a
|
|
13
|
+
* rail full of links that all lead back here would suggest otherwise. The
|
|
14
|
+
* sections that appear afterwards are in sections.ts, the frame they share in
|
|
15
|
+
* layout.ts.
|
|
16
|
+
*/
|
|
17
|
+
import { layout, escapeHtml, errorBanner } from './layout.js';
|
|
18
|
+
import { EXPOSURE_LABELS, } from '../credentials/store.js';
|
|
19
|
+
export { escapeHtml, formatDuration } from './layout.js';
|
|
20
|
+
function exposureTag(exposure) {
|
|
21
|
+
return `<span class="tag exposure-${exposure}">${escapeHtml(EXPOSURE_LABELS[exposure])}</span>`;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* One storage option, as a radio inside the sign-in form.
|
|
25
|
+
*
|
|
26
|
+
* It used to be a form of its own that posted the choice and re-rendered the
|
|
27
|
+
* page. That worked and read badly: picking one of four options reloaded
|
|
28
|
+
* everything, and a page that flashes on every click feels like something went
|
|
29
|
+
* wrong. Now the choice travels with the credentials in one submission, and
|
|
30
|
+
* nothing happens until Connect is pressed.
|
|
31
|
+
*
|
|
32
|
+
* The whole card is the label, so the click target is the card rather than a
|
|
33
|
+
* small circle. The radio itself stays a real radio: keyboard and screen readers
|
|
34
|
+
* get the ordinary control, and the styling is a ring around the card.
|
|
35
|
+
*/
|
|
36
|
+
function storeOption(store, suggested, chosen) {
|
|
37
|
+
const classes = ['option'];
|
|
38
|
+
if (!store.available)
|
|
39
|
+
classes.push('unavailable');
|
|
40
|
+
const id = `store-${store.kind}`;
|
|
41
|
+
return `<label class="${classes.join(' ')}" for="${id}">
|
|
42
|
+
<input type="radio" name="store" id="${id}" value="${escapeHtml(store.kind)}"
|
|
43
|
+
${chosen ? 'checked' : ''} ${store.available ? '' : 'disabled'}>
|
|
44
|
+
<div class="option-body">
|
|
45
|
+
<header>
|
|
46
|
+
<h3>${escapeHtml(store.title)}</h3>
|
|
47
|
+
${exposureTag(store.exposure)}
|
|
48
|
+
<span class="tag">prompts: ${escapeHtml(store.prompts)}</span>
|
|
49
|
+
${suggested ? '<span class="tag suggested">suggested</span>' : ''}
|
|
50
|
+
</header>
|
|
51
|
+
<p class="hint">${escapeHtml(store.summary)}</p>
|
|
52
|
+
<p class="pro">${escapeHtml(store.benefit)}</p>
|
|
53
|
+
<p class="con">${escapeHtml(store.cost)}</p>
|
|
54
|
+
<p class="who">${escapeHtml(store.bestFor)}</p>
|
|
55
|
+
${store.reason ? `<p class="blocked">${escapeHtml(store.reason)}</p>` : ''}
|
|
56
|
+
</div>
|
|
57
|
+
</label>`;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The warning shown when the plain file is picked.
|
|
61
|
+
*
|
|
62
|
+
* The card already states the cost in one line, and one line is easy to read
|
|
63
|
+
* past when it sits between three other options. This one is in the way, which
|
|
64
|
+
* is the point: it is the only option that leaves a usable password where
|
|
65
|
+
* anything that can read your files can read it.
|
|
66
|
+
*
|
|
67
|
+
* It is not a refusal. The choice stays available and the warning closes, both
|
|
68
|
+
* because the trade is sometimes the right one and because nobody is served by
|
|
69
|
+
* a tool that decides for them. Shown and closed entirely in CSS, since there is
|
|
70
|
+
* no JavaScript here to open a dialog with.
|
|
71
|
+
*/
|
|
72
|
+
function plainFileWarning() {
|
|
73
|
+
return `<div class="warning-layer">
|
|
74
|
+
<label for="warning-seen" class="warning-backdrop" aria-hidden="true"></label>
|
|
75
|
+
<div class="warning-box">
|
|
76
|
+
<h3>The password will be readable</h3>
|
|
77
|
+
<p>A plain file keeps your Bridge password as text. The file is created with mode
|
|
78
|
+
<code>600</code>, so only your user account can read it, and that is the whole of the
|
|
79
|
+
protection: <strong>anything running as you can read it too</strong>, including a program
|
|
80
|
+
you did not mean to run.</p>
|
|
81
|
+
<p class="hint">The other three options all avoid this. The encrypted file works everywhere
|
|
82
|
+
the plain one does and asks for a master password once per start.</p>
|
|
83
|
+
<label for="warning-seen" class="warning-close">I understand, keep this option</label>
|
|
84
|
+
</div>
|
|
85
|
+
</div>`;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The extra fields the encrypted file needs.
|
|
89
|
+
*
|
|
90
|
+
* Always in the document now, and shown by CSS when that option is selected.
|
|
91
|
+
* They used to be rendered only for that store, which meant selecting it had to
|
|
92
|
+
* reload the page to make them appear.
|
|
93
|
+
*
|
|
94
|
+
* Two fields rather than one: a mistyped master password that nothing anywhere
|
|
95
|
+
* can recover is worth one extra line of typing.
|
|
96
|
+
*
|
|
97
|
+
* They are not disabled while hidden, and that is deliberate. A hidden field
|
|
98
|
+
* submits its value, so a master password typed and then abandoned by switching
|
|
99
|
+
* options would still travel. The server ignores it for any other store, and
|
|
100
|
+
* ignoring it there is the one place that decision belongs.
|
|
101
|
+
*/
|
|
102
|
+
function masterPasswordFields() {
|
|
103
|
+
return `<div class="master-fields">
|
|
104
|
+
<label for="master">Master password for the encrypted file</label>
|
|
105
|
+
<input id="master" name="master" type="password" autocomplete="new-password">
|
|
106
|
+
<p class="hint">Chosen by you, and asked for again every time this server starts. It is not stored
|
|
107
|
+
anywhere: <strong>if you forget it, the saved Bridge password is gone</strong> and you have to
|
|
108
|
+
fetch it from the Bridge again.</p>
|
|
109
|
+
|
|
110
|
+
<label for="masterRepeat">Repeat the master password</label>
|
|
111
|
+
<input id="masterRepeat" name="masterRepeat" type="password" autocomplete="new-password">
|
|
112
|
+
</div>`;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Where the Bridge is reached, folded away.
|
|
116
|
+
*
|
|
117
|
+
* Almost nobody needs it: the Bridge listens on 1143 and 1025 unless somebody
|
|
118
|
+
* changed that in the Bridge itself. But a wrong port looks exactly like a
|
|
119
|
+
* Bridge that is not running, and finding that out after signing in, in a
|
|
120
|
+
* different section, is a poor way to spend an evening. So it is here, prefilled
|
|
121
|
+
* with what the server will actually use, and closed.
|
|
122
|
+
*
|
|
123
|
+
* `<details>` rather than a scripted panel: it is the browser's own disclosure
|
|
124
|
+
* element and needs nothing this page cannot have.
|
|
125
|
+
*
|
|
126
|
+
* **The host is shown and not editable, and that is deliberate.** A wrong port
|
|
127
|
+
* means nothing works, which is obvious immediately. A wrong host means the
|
|
128
|
+
* Bridge password is handed to another machine at the next connection, which is
|
|
129
|
+
* obvious to nobody. Making it changeable is tracked in #19, where it will sit
|
|
130
|
+
* behind a key that never travels through a tool call, so that changing it stays
|
|
131
|
+
* a deliberate act by the person at the keyboard.
|
|
132
|
+
*/
|
|
133
|
+
function advancedSettings(data) {
|
|
134
|
+
const portFields = data.portsLocked
|
|
135
|
+
? `<p class="hint">Both ports come from the environment
|
|
136
|
+
(<code>BRIDGE_IMAP_PORT</code>, <code>BRIDGE_SMTP_PORT</code>), so they are not changed here.
|
|
137
|
+
An explicit setting stays explicit.</p>
|
|
138
|
+
<table>
|
|
139
|
+
<tr><th>IMAP port</th><td>${escapeHtml(String(data.bridgeImapPort))}</td></tr>
|
|
140
|
+
<tr><th>SMTP port</th><td>${escapeHtml(String(data.bridgeSmtpPort))}</td></tr>
|
|
141
|
+
</table>`
|
|
142
|
+
: `<label for="imapPort">IMAP port</label>
|
|
143
|
+
<input id="imapPort" name="imapPort" type="text" inputmode="numeric"
|
|
144
|
+
value="${escapeHtml(String(data.bridgeImapPort))}">
|
|
145
|
+
|
|
146
|
+
<label for="smtpPort">SMTP port</label>
|
|
147
|
+
<input id="smtpPort" name="smtpPort" type="text" inputmode="numeric"
|
|
148
|
+
value="${escapeHtml(String(data.bridgeSmtpPort))}">
|
|
149
|
+
<p class="hint">The defaults are what the Bridge uses unless you changed them in the Bridge
|
|
150
|
+
itself, where you also find the real ones under its settings. They are saved along with the
|
|
151
|
+
sign-in and can be changed later under Bridge.</p>`;
|
|
152
|
+
return `<details class="card advanced"${data.showAdvanced ? ' open' : ''}>
|
|
153
|
+
<summary>Advanced: where the Bridge is reached</summary>
|
|
154
|
+
|
|
155
|
+
<label for="bridgeHostShown">Address of the Bridge</label>
|
|
156
|
+
<input id="bridgeHostShown" type="text" value="${escapeHtml(data.bridgeHost)}" disabled>
|
|
157
|
+
<p class="hint">Set with <code>BRIDGE_HOST</code> and not editable here yet. A wrong port means
|
|
158
|
+
nothing works and you notice at once; a wrong address means your Bridge password goes to another
|
|
159
|
+
machine and nobody notices at all. Making it changeable in the browser is planned, behind a key
|
|
160
|
+
that no assistant can reach.</p>
|
|
161
|
+
|
|
162
|
+
${portFields}
|
|
163
|
+
</details>`;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* The sign-in form.
|
|
167
|
+
*
|
|
168
|
+
* One form for everything: the address, the password and where the password
|
|
169
|
+
* goes. Nothing is submitted until Connect is pressed, so choosing among the
|
|
170
|
+
* four options costs no round trip and the page does not move under the
|
|
171
|
+
* pointer.
|
|
172
|
+
*
|
|
173
|
+
* Two things appear and disappear with the selection, both in CSS: the master
|
|
174
|
+
* password fields for the encrypted file, and the warning for the plain file.
|
|
175
|
+
* See the stylesheet for how, and for what happens in a browser too old to do
|
|
176
|
+
* it.
|
|
177
|
+
*/
|
|
178
|
+
export function loginPage(data) {
|
|
179
|
+
const effective = data.chosen ?? data.suggested;
|
|
180
|
+
const options = data.stores
|
|
181
|
+
.map((store) => storeOption(store, store.kind === data.suggested, store.kind === effective))
|
|
182
|
+
.join('\n');
|
|
183
|
+
return layout({
|
|
184
|
+
title: 'Sign in',
|
|
185
|
+
section: 'none',
|
|
186
|
+
token: data.token,
|
|
187
|
+
disclaimer: data.disclaimer,
|
|
188
|
+
body: `<h1>Connect to Proton Mail Bridge</h1>
|
|
189
|
+
<p class="lead">Enter the credentials the Bridge generated for your account. This page runs on your
|
|
190
|
+
own machine and the password is never sent anywhere else.</p>
|
|
191
|
+
|
|
192
|
+
${errorBanner(data.error)}
|
|
193
|
+
|
|
194
|
+
<form method="post" action="/sign-in?token=${escapeHtml(data.token)}" class="sign-in">
|
|
195
|
+
<input type="hidden" name="csrf" value="${escapeHtml(data.csrf)}">
|
|
196
|
+
<input type="checkbox" id="warning-seen" class="offscreen">
|
|
197
|
+
|
|
198
|
+
<div class="card">
|
|
199
|
+
<label for="address">Proton address</label>
|
|
200
|
+
<input id="address" name="address" type="text" autocomplete="off" spellcheck="false"
|
|
201
|
+
value="${escapeHtml(data.address ?? '')}" placeholder="you@example.com">
|
|
202
|
+
<p class="hint">The address as it appears in the Bridge.</p>
|
|
203
|
+
|
|
204
|
+
<label for="password">Bridge password</label>
|
|
205
|
+
<input id="password" name="password" type="password" autocomplete="off">
|
|
206
|
+
<p class="hint"><strong>Not your Proton account password.</strong> The Bridge generates a separate
|
|
207
|
+
one per account. You find it in the Bridge application under the account, or by running
|
|
208
|
+
<code>protonmail-bridge --cli</code> and then <code>info</code>.</p>
|
|
209
|
+
|
|
210
|
+
${masterPasswordFields()}
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
<h2>Where should the password be kept?</h2>
|
|
214
|
+
<p class="lead">${escapeHtml(data.suggestionReason)}</p>
|
|
215
|
+
${options}
|
|
216
|
+
|
|
217
|
+
${advancedSettings(data)}
|
|
218
|
+
|
|
219
|
+
<div class="card">
|
|
220
|
+
<button type="submit">Connect</button>
|
|
221
|
+
<p class="hint">Nothing is sent until you press this. The Bridge is asked whether the credentials
|
|
222
|
+
work before they are stored anywhere.</p>
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
${plainFileWarning()}
|
|
226
|
+
</form>`,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Asked for when an encrypted credentials file exists but is not open yet.
|
|
231
|
+
*
|
|
232
|
+
* The counterpart to the sign-in form, and the reason the encrypted option is
|
|
233
|
+
* usable at all: after a restart the Bridge password is still on disk, so the
|
|
234
|
+
* only thing missing is the one password that decrypts it.
|
|
235
|
+
*
|
|
236
|
+
* It also offers the way out. Someone who forgot their master password cannot
|
|
237
|
+
* be left on a page that only accepts the thing they do not have.
|
|
238
|
+
*/
|
|
239
|
+
export function unlockPage(data) {
|
|
240
|
+
return layout({
|
|
241
|
+
title: 'Unlock',
|
|
242
|
+
section: 'none',
|
|
243
|
+
token: data.token,
|
|
244
|
+
disclaimer: data.disclaimer,
|
|
245
|
+
body: `<h1>Unlock the stored credentials</h1>
|
|
246
|
+
<p class="lead">Your Bridge password is saved in an encrypted file. Enter the master password you
|
|
247
|
+
chose for it and the server can connect again.</p>
|
|
248
|
+
|
|
249
|
+
${errorBanner(data.error)}
|
|
250
|
+
|
|
251
|
+
<div class="card">
|
|
252
|
+
<form method="post" action="/unlock?token=${escapeHtml(data.token)}">
|
|
253
|
+
<input type="hidden" name="csrf" value="${escapeHtml(data.csrf)}">
|
|
254
|
+
<label for="master">Master password</label>
|
|
255
|
+
<input id="master" name="master" type="password" autocomplete="current-password" autofocus>
|
|
256
|
+
<p class="hint">The file is at <code>${escapeHtml(data.path)}</code>. Nothing but this password
|
|
257
|
+
can open it, not even this server.</p>
|
|
258
|
+
<button type="submit">Unlock</button>
|
|
259
|
+
</form>
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
<div class="card">
|
|
263
|
+
<h2>Forgot the master password?</h2>
|
|
264
|
+
<p>Then the file cannot be opened, by anyone. Throwing it away is the only way forward. You will
|
|
265
|
+
have to look your Bridge password up in the Bridge again and sign in from scratch.</p>
|
|
266
|
+
<form method="post" action="/discard-encrypted?token=${escapeHtml(data.token)}">
|
|
267
|
+
<input type="hidden" name="csrf" value="${escapeHtml(data.csrfDiscard)}">
|
|
268
|
+
<button type="submit" class="danger">Delete the file and start over</button>
|
|
269
|
+
</form>
|
|
270
|
+
</div>`,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
/** Shown when a request is rejected. Deliberately says little. */
|
|
274
|
+
export function rejectionPage() {
|
|
275
|
+
return layout({
|
|
276
|
+
title: 'Not available',
|
|
277
|
+
section: 'none',
|
|
278
|
+
body: `<h1>Not available</h1>
|
|
279
|
+
<p>This request was refused. Either the link is wrong or it has expired.</p>
|
|
280
|
+
<p class="hint">The address for this interface is printed by the server when it asks you to sign in,
|
|
281
|
+
and it changes every time the server restarts.</p>`,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
//# sourceMappingURL=pages.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pages.js","sourceRoot":"","sources":["../../src/web/pages.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sections shown once somebody is signed in.
|
|
3
|
+
*
|
|
4
|
+
* One page each, rather than one long page with headings. The split is not
|
|
5
|
+
* cosmetic: it decides what is on screen without being asked for. Mailbox names
|
|
6
|
+
* had that treatment already, for reasons written at mailboxSection, and the
|
|
7
|
+
* same reasoning applies more weakly to everything else here. A page is also a
|
|
8
|
+
* place to put a setting, and the settings that limit what the agent may do are
|
|
9
|
+
* arriving next.
|
|
10
|
+
*
|
|
11
|
+
* The counterpart is pages.ts, which holds what comes before signing in.
|
|
12
|
+
*/
|
|
13
|
+
import { type DisclaimerState } from './layout.js';
|
|
14
|
+
import type { StoreKind } from '../credentials/store.js';
|
|
15
|
+
/** What every section needs to draw itself. */
|
|
16
|
+
interface Common {
|
|
17
|
+
token: string;
|
|
18
|
+
error?: string;
|
|
19
|
+
notice?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Required rather than optional on purpose. Forgetting it would make the
|
|
22
|
+
* notice quietly disappear, and a legal statement is not something to lose by
|
|
23
|
+
* omission.
|
|
24
|
+
*/
|
|
25
|
+
disclaimer: DisclaimerState;
|
|
26
|
+
}
|
|
27
|
+
export interface OverviewData extends Common {
|
|
28
|
+
connected: boolean;
|
|
29
|
+
address?: string;
|
|
30
|
+
storeTitle?: string;
|
|
31
|
+
readOnly: boolean;
|
|
32
|
+
webPort: number;
|
|
33
|
+
uptimeMs: number;
|
|
34
|
+
mailboxCount?: number;
|
|
35
|
+
messageCount?: number;
|
|
36
|
+
unseenCount?: number;
|
|
37
|
+
bridgeHost: string;
|
|
38
|
+
bridgeImapPort: number;
|
|
39
|
+
bridgeSmtpPort: number;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The first page, and the only one that answers "is this working".
|
|
43
|
+
*
|
|
44
|
+
* It carries state and no controls. Every button lives in the section it belongs
|
|
45
|
+
* to, so that a page somebody opens to check something cannot be the page where
|
|
46
|
+
* they change it by accident.
|
|
47
|
+
*/
|
|
48
|
+
export declare function overviewSection(data: OverviewData): string;
|
|
49
|
+
export interface BridgeData extends Common {
|
|
50
|
+
csrfSettings: string;
|
|
51
|
+
csrfTest: string;
|
|
52
|
+
bridgeHost: string;
|
|
53
|
+
bridgeImapPort: number;
|
|
54
|
+
bridgeSmtpPort: number;
|
|
55
|
+
/** True when the port came from the environment and cannot be changed here. */
|
|
56
|
+
portsLocked: boolean;
|
|
57
|
+
certificate?: {
|
|
58
|
+
fingerprint: string;
|
|
59
|
+
subject: string;
|
|
60
|
+
firstSeen: string;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** Ports, the pinned certificate, and the button that tries the Bridge. */
|
|
64
|
+
export declare function bridgeSection(data: BridgeData): string;
|
|
65
|
+
export interface CredentialsData extends Common {
|
|
66
|
+
csrfSignOut: string;
|
|
67
|
+
address?: string;
|
|
68
|
+
storeKind?: StoreKind;
|
|
69
|
+
storeTitle?: string;
|
|
70
|
+
}
|
|
71
|
+
/** Where the Bridge password is kept, and the way to take it back out. */
|
|
72
|
+
export declare function credentialsSection(data: CredentialsData): string;
|
|
73
|
+
export interface ActivityData extends Common {
|
|
74
|
+
/** Calls in progress, so a slow search is visibly a slow search. */
|
|
75
|
+
running: Array<{
|
|
76
|
+
tool: string;
|
|
77
|
+
forMs: number;
|
|
78
|
+
}>;
|
|
79
|
+
/** Finished calls, newest first. */
|
|
80
|
+
recent: Array<{
|
|
81
|
+
tool: string;
|
|
82
|
+
at: number;
|
|
83
|
+
durationMs: number;
|
|
84
|
+
outcome: 'ok' | 'failed';
|
|
85
|
+
}>;
|
|
86
|
+
}
|
|
87
|
+
/** What the server has been doing, in tool names and timings and nothing else. */
|
|
88
|
+
export declare function activitySection(data: ActivityData): string;
|
|
89
|
+
export interface MailboxData extends Common {
|
|
90
|
+
mailboxes: Array<{
|
|
91
|
+
path: string;
|
|
92
|
+
kind: 'system' | 'folder' | 'label';
|
|
93
|
+
selectable: boolean;
|
|
94
|
+
}>;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The folders and labels, on their own page.
|
|
98
|
+
*
|
|
99
|
+
* Its own page because of what these names are. They belong to the Proton
|
|
100
|
+
* account, not the address that signed in, so this is the full structure of the
|
|
101
|
+
* account even when the mail in it is invisible. Showing that as part of an
|
|
102
|
+
* answer nobody asked for would hand it over unasked.
|
|
103
|
+
*/
|
|
104
|
+
export declare function mailboxSection(data: MailboxData): string;
|
|
105
|
+
export {};
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sections shown once somebody is signed in.
|
|
3
|
+
*
|
|
4
|
+
* One page each, rather than one long page with headings. The split is not
|
|
5
|
+
* cosmetic: it decides what is on screen without being asked for. Mailbox names
|
|
6
|
+
* had that treatment already, for reasons written at mailboxSection, and the
|
|
7
|
+
* same reasoning applies more weakly to everything else here. A page is also a
|
|
8
|
+
* place to put a setting, and the settings that limit what the agent may do are
|
|
9
|
+
* arriving next.
|
|
10
|
+
*
|
|
11
|
+
* The counterpart is pages.ts, which holds what comes before signing in.
|
|
12
|
+
*/
|
|
13
|
+
import { layout, escapeHtml, formatDuration, errorBanner, noticeBanner, } from './layout.js';
|
|
14
|
+
/** A wall-clock time, local to whoever is looking at the page. */
|
|
15
|
+
function formatTime(at) {
|
|
16
|
+
return new Date(at).toLocaleTimeString();
|
|
17
|
+
}
|
|
18
|
+
/** A row of the definition tables that carry most of the state here. */
|
|
19
|
+
function row(key, value, raw = false) {
|
|
20
|
+
return `<tr><th>${escapeHtml(key)}</th><td>${raw ? value : escapeHtml(value)}</td></tr>`;
|
|
21
|
+
}
|
|
22
|
+
function frame(section, title, data, body) {
|
|
23
|
+
return layout({
|
|
24
|
+
title,
|
|
25
|
+
section,
|
|
26
|
+
token: data.token,
|
|
27
|
+
disclaimer: data.disclaimer,
|
|
28
|
+
body: `${errorBanner(data.error)}${noticeBanner(data.notice)}${body}`,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The first page, and the only one that answers "is this working".
|
|
33
|
+
*
|
|
34
|
+
* It carries state and no controls. Every button lives in the section it belongs
|
|
35
|
+
* to, so that a page somebody opens to check something cannot be the page where
|
|
36
|
+
* they change it by accident.
|
|
37
|
+
*/
|
|
38
|
+
export function overviewSection(data) {
|
|
39
|
+
const connection = data.connected
|
|
40
|
+
? '<span class="state good">connected</span>'
|
|
41
|
+
: '<span class="state bad">not connected</span>';
|
|
42
|
+
const rows = [
|
|
43
|
+
row('Connection', connection, true),
|
|
44
|
+
row('Address', data.address ?? 'not signed in'),
|
|
45
|
+
row('Credentials kept in', data.storeTitle ?? 'nowhere yet'),
|
|
46
|
+
row('Mode', data.readOnly ? 'read-only, nothing can be changed or sent' : 'read and write'),
|
|
47
|
+
row('Bridge IMAP', `${data.bridgeHost}:${data.bridgeImapPort}`),
|
|
48
|
+
row('Bridge SMTP', `${data.bridgeHost}:${data.bridgeSmtpPort}`),
|
|
49
|
+
row('This interface', `127.0.0.1:${data.webPort}`),
|
|
50
|
+
row('Server running for', formatDuration(data.uptimeMs)),
|
|
51
|
+
];
|
|
52
|
+
const counters = [];
|
|
53
|
+
if (data.mailboxCount !== undefined)
|
|
54
|
+
counters.push(row('Mailboxes', String(data.mailboxCount)));
|
|
55
|
+
if (data.messageCount !== undefined) {
|
|
56
|
+
const unseen = data.unseenCount === undefined ? '' : ` (${data.unseenCount} unread)`;
|
|
57
|
+
counters.push(row('Messages in inbox', `${data.messageCount}${unseen}`));
|
|
58
|
+
}
|
|
59
|
+
return frame('overview', 'Overview', data, `<h1>Overview</h1>
|
|
60
|
+
<p class="lead">What this server currently knows. No message content is shown anywhere in this
|
|
61
|
+
interface: it is for configuration and state, not a mail client.</p>
|
|
62
|
+
|
|
63
|
+
<div class="card"><table>${rows.join('\n')}</table></div>
|
|
64
|
+
${counters.length > 0 ? `<h2>Mailbox counts</h2>\n<div class="card"><table>${counters.join('\n')}</table></div>` : ''}`);
|
|
65
|
+
}
|
|
66
|
+
/** Ports, the pinned certificate, and the button that tries the Bridge. */
|
|
67
|
+
export function bridgeSection(data) {
|
|
68
|
+
const ports = data.portsLocked
|
|
69
|
+
? `<p class="hint">The ports come from the environment (<code>BRIDGE_IMAP_PORT</code> or
|
|
70
|
+
<code>BRIDGE_SMTP_PORT</code>), so they are not changed here. An explicit setting stays
|
|
71
|
+
explicit.</p>`
|
|
72
|
+
: `<form method="post" action="/settings?token=${escapeHtml(data.token)}">
|
|
73
|
+
<input type="hidden" name="csrf" value="${escapeHtml(data.csrfSettings)}">
|
|
74
|
+
<label for="imapPort">IMAP port</label>
|
|
75
|
+
<input id="imapPort" name="imapPort" type="text" inputmode="numeric"
|
|
76
|
+
value="${escapeHtml(String(data.bridgeImapPort))}">
|
|
77
|
+
<label for="smtpPort">SMTP port</label>
|
|
78
|
+
<input id="smtpPort" name="smtpPort" type="text" inputmode="numeric"
|
|
79
|
+
value="${escapeHtml(String(data.bridgeSmtpPort))}">
|
|
80
|
+
<p class="hint">The Bridge lets you change these, so a wrong value here looks exactly like a Bridge
|
|
81
|
+
that is not running. You find the real ones in the Bridge under settings. The host is not
|
|
82
|
+
changeable here on purpose: it would be a field that sends your Bridge password to another
|
|
83
|
+
machine.</p>
|
|
84
|
+
<button type="submit">Save ports</button>
|
|
85
|
+
</form>`;
|
|
86
|
+
const certificate = data.certificate
|
|
87
|
+
? `<div class="card">
|
|
88
|
+
<table>
|
|
89
|
+
${row('Fingerprint (SHA-256)', `<code>${escapeHtml(data.certificate.fingerprint)}</code>`, true)}
|
|
90
|
+
${row('Subject', data.certificate.subject)}
|
|
91
|
+
${row('First seen', data.certificate.firstSeen.slice(0, 10))}
|
|
92
|
+
</table>
|
|
93
|
+
<p class="hint">The Bridge signs its own certificate, so it was recorded on first use and is checked
|
|
94
|
+
against that record every time. If it ever changes, the server refuses to connect rather than hand
|
|
95
|
+
your password to whatever is answering on that port.</p>
|
|
96
|
+
</div>`
|
|
97
|
+
: `<div class="card"><p class="hint">Nothing recorded yet. The certificate is read and pinned on the
|
|
98
|
+
first connection to the Bridge.</p></div>`;
|
|
99
|
+
return frame('bridge', 'Bridge', data, `<h1>Bridge</h1>
|
|
100
|
+
<p class="lead">Where the Proton Mail Bridge is reached, and the certificate this server holds it
|
|
101
|
+
to. The host stays at <code>${escapeHtml(data.bridgeHost)}</code> and cannot be changed here.</p>
|
|
102
|
+
|
|
103
|
+
<h2>Connection test</h2>
|
|
104
|
+
<div class="card">
|
|
105
|
+
<form method="post" action="/test-connection?token=${escapeHtml(data.token)}">
|
|
106
|
+
<input type="hidden" name="csrf" value="${escapeHtml(data.csrfTest)}">
|
|
107
|
+
<p>Checks that the Bridge answers on the ports below and accepts the stored credentials.</p>
|
|
108
|
+
<button type="submit">Test the connection</button>
|
|
109
|
+
</form>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<h2>Ports</h2>
|
|
113
|
+
<div class="card">${ports}</div>
|
|
114
|
+
|
|
115
|
+
<h2>Certificate</h2>
|
|
116
|
+
${certificate}`);
|
|
117
|
+
}
|
|
118
|
+
/** Where the Bridge password is kept, and the way to take it back out. */
|
|
119
|
+
export function credentialsSection(data) {
|
|
120
|
+
return frame('credentials', 'Credentials', data, `<h1>Credentials</h1>
|
|
121
|
+
<p class="lead">The Bridge password this server signs in with, and where it is kept between
|
|
122
|
+
restarts.</p>
|
|
123
|
+
|
|
124
|
+
<div class="card"><table>
|
|
125
|
+
${row('Signed in as', data.address ?? 'not signed in')}
|
|
126
|
+
${row('Kept in', data.storeTitle ?? 'nowhere yet')}
|
|
127
|
+
</table></div>
|
|
128
|
+
|
|
129
|
+
<h2>Sign out</h2>
|
|
130
|
+
<div class="card">
|
|
131
|
+
<form method="post" action="/sign-out?token=${escapeHtml(data.token)}">
|
|
132
|
+
<input type="hidden" name="csrf" value="${escapeHtml(data.csrfSignOut)}">
|
|
133
|
+
<p>Removes the stored credentials from wherever they are kept, everywhere at once, and
|
|
134
|
+
disconnects.</p>
|
|
135
|
+
<button type="submit" class="danger">Sign out and forget credentials</button>
|
|
136
|
+
</form>
|
|
137
|
+
</div>`);
|
|
138
|
+
}
|
|
139
|
+
/** What the server has been doing, in tool names and timings and nothing else. */
|
|
140
|
+
export function activitySection(data) {
|
|
141
|
+
const running = data.running.length === 0
|
|
142
|
+
? '<p class="hint">Nothing running right now.</p>'
|
|
143
|
+
: `<table>
|
|
144
|
+
<tr><th>In progress</th><th>Running for</th></tr>
|
|
145
|
+
${data.running
|
|
146
|
+
.map((call) => `<tr><td><code>${escapeHtml(call.tool)}</code></td><td>${escapeHtml(formatDuration(call.forMs))}</td></tr>`)
|
|
147
|
+
.join('\n ')}
|
|
148
|
+
</table>
|
|
149
|
+
<p class="hint">A full-text search over a large mailbox takes a few seconds. This is how you tell
|
|
150
|
+
that apart from a server that has stopped answering.</p>`;
|
|
151
|
+
const recent = data.recent.length === 0
|
|
152
|
+
? '<p class="hint">No tool has been called yet in this session.</p>'
|
|
153
|
+
: `<table>
|
|
154
|
+
<tr><th>Tool</th><th>When</th><th>Took</th><th>Result</th></tr>
|
|
155
|
+
${data.recent
|
|
156
|
+
.map((call) => `<tr><td><code>${escapeHtml(call.tool)}</code></td><td>${escapeHtml(formatTime(call.at))}</td>` +
|
|
157
|
+
`<td>${escapeHtml(formatDuration(call.durationMs))}</td>` +
|
|
158
|
+
`<td>${call.outcome === 'ok' ? 'ok' : 'failed'}</td></tr>`)
|
|
159
|
+
.join('\n ')}
|
|
160
|
+
</table>`;
|
|
161
|
+
return frame('activity', 'Activity', data, `<h1>Activity</h1>
|
|
162
|
+
<p class="lead">Which tools were called and how long they took. This is the record of what an
|
|
163
|
+
assistant did with the connection.</p>
|
|
164
|
+
|
|
165
|
+
<h2>Right now</h2>
|
|
166
|
+
<div class="card">${running}</div>
|
|
167
|
+
|
|
168
|
+
<h2>Finished calls</h2>
|
|
169
|
+
<div class="card">
|
|
170
|
+
${recent}
|
|
171
|
+
<p class="hint">Tool names and timings only. What was searched for, which message was read and which
|
|
172
|
+
mailbox was opened are deliberately not recorded: that would turn this page into a log of your mail
|
|
173
|
+
habits.</p>
|
|
174
|
+
</div>`);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* The folders and labels, on their own page.
|
|
178
|
+
*
|
|
179
|
+
* Its own page because of what these names are. They belong to the Proton
|
|
180
|
+
* account, not the address that signed in, so this is the full structure of the
|
|
181
|
+
* account even when the mail in it is invisible. Showing that as part of an
|
|
182
|
+
* answer nobody asked for would hand it over unasked.
|
|
183
|
+
*/
|
|
184
|
+
export function mailboxSection(data) {
|
|
185
|
+
const groups = [
|
|
186
|
+
['system', 'System mailboxes'],
|
|
187
|
+
['folder', 'Folders'],
|
|
188
|
+
['label', 'Labels'],
|
|
189
|
+
];
|
|
190
|
+
const sections = groups
|
|
191
|
+
.map(([kind, heading]) => {
|
|
192
|
+
const part = data.mailboxes.filter((m) => m.kind === kind && m.selectable);
|
|
193
|
+
if (part.length === 0)
|
|
194
|
+
return '';
|
|
195
|
+
return `<h2>${escapeHtml(heading)} (${part.length})</h2>
|
|
196
|
+
<div class="card"><table>
|
|
197
|
+
${part.map((m) => `<tr><td><code>${escapeHtml(m.path)}</code></td></tr>`).join('\n')}
|
|
198
|
+
</table></div>`;
|
|
199
|
+
})
|
|
200
|
+
.filter(Boolean)
|
|
201
|
+
.join('\n');
|
|
202
|
+
return frame('mailboxes', 'Folders and labels', data, `<h1>Folders and labels</h1>
|
|
203
|
+
<p class="lead">Names only, never contents. Proton distinguishes folders, where a message lives in
|
|
204
|
+
exactly one, from labels, where it can carry any number.</p>
|
|
205
|
+
${sections || '<div class="card"><p class="hint">No mailboxes were returned.</p></div>'}`);
|
|
206
|
+
}
|
|
207
|
+
//# sourceMappingURL=sections.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sections.js","sourceRoot":"","sources":["../../src/web/sections.ts"],"names":[],"mappings":"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"}
|