feat(mailboxes): postkasser kan forbindes fra fladen
Release / Build and publish image (push) Failing after 9m59s
Release / Build and publish image (push) Failing after 9m59s
Panelet under Integrationer: tilføj, hent nu, kobl fra. Sit eget frem for en tilstand mere inde i Gmail-panelet, for de to er ikke det samme i forskellige klæder — det ene er en app, man logger ind igennem, det andet et kodeord, man selv holder. Feltet hedder IMAP-server og ikke Server: der stod i forvejen Server på CalDAV længere nede, og to felter med samme navn på én side er tvetydige også for en skærmlæser. Testen fandt det, men det var ikke kun testens problem. To vagter fangede mangler undervejs, og begge havde ret: mailbox_failed skulle have en tekst i fladen, ellers ville nogen få serverens engelske logsprog at se, og de tre ruter skulle i openapi.yaml. Der er nu et Mailbox-skema samme sted, som siger hvad der ikke kommer tilbage — kodeordet og eventuelle tokens. E2E-prøven forbinder til noget, der ikke lytter, og ser efter værtens egen afvisning på skærmen. Den er samtidig prøven på at 422 var det rigtige valg: var det stadig 502, ville et mellemled kunne kaste beskeden væk, og der ville stå "noget gik galt". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
6b4ef5b2f2
commit
c9dfffd95a
@@ -182,6 +182,73 @@
|
||||
app.toast(t('int.copyFailed'));
|
||||
}
|
||||
}
|
||||
|
||||
// --- mailboxes ----------------------------------------------------------------
|
||||
|
||||
let mailboxes = $state([]);
|
||||
let addingMailbox = $state(false);
|
||||
let connecting = $state(false);
|
||||
let syncingId = $state(null);
|
||||
let newMailbox = $state({ host: '', username: '', password: '' });
|
||||
|
||||
$effect(() => {
|
||||
loadMailboxes();
|
||||
});
|
||||
|
||||
async function loadMailboxes() {
|
||||
try {
|
||||
mailboxes = (await api.mailboxes()).mailboxes ?? [];
|
||||
} catch {
|
||||
// A settings page that will not render because one panel could not load
|
||||
// is worse than a panel that is empty.
|
||||
mailboxes = [];
|
||||
}
|
||||
}
|
||||
|
||||
async function addMailbox(event) {
|
||||
event.preventDefault();
|
||||
connecting = true;
|
||||
try {
|
||||
// The server dials before it saves, so an error here is the host's own
|
||||
// refusal — a wrong app password, a host that is not listening.
|
||||
await api.addMailbox({ ...newMailbox });
|
||||
newMailbox = { host: '', username: '', password: '' };
|
||||
addingMailbox = false;
|
||||
await loadMailboxes();
|
||||
app.toast(t('int.mailboxConnected'));
|
||||
} catch (e) {
|
||||
app.toast(humanMessage(e));
|
||||
} finally {
|
||||
connecting = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function syncMailbox(box) {
|
||||
syncingId = box.id;
|
||||
try {
|
||||
const result = await api.syncMailbox(box.id);
|
||||
app.toast(
|
||||
result?.created
|
||||
? plural(result.created, 'int.gmailFetchedOne', 'int.gmailFetchedMany')
|
||||
: t('int.gmailNothingNew')
|
||||
);
|
||||
await loadMailboxes();
|
||||
} catch (e) {
|
||||
app.toast(humanMessage(e));
|
||||
} finally {
|
||||
syncingId = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function removeMailbox(box) {
|
||||
if (!confirm(t('int.disconnectMailbox', { name: box.name }))) return;
|
||||
try {
|
||||
await api.deleteMailbox(box.id);
|
||||
await loadMailboxes();
|
||||
} catch (e) {
|
||||
app.toast(humanMessage(e));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="panel">
|
||||
@@ -352,6 +419,66 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Mailboxes read over IMAP: iCloud, Fastmail, any ordinary host. Its own panel
|
||||
rather than a second mode inside the Gmail one, because they are not the same
|
||||
thing wearing different clothes — one is an app you sign in through, the other
|
||||
is a password you hold. -->
|
||||
<section class="panel">
|
||||
<header>
|
||||
<h2>{t('int.mailboxes')}</h2>
|
||||
<p class="hint">{t('int.mailboxesHint')}</p>
|
||||
</header>
|
||||
|
||||
{#if mailboxes.length}
|
||||
<ul class="mailboxes">
|
||||
{#each mailboxes as box (box.id)}
|
||||
<li>
|
||||
<div class="what">
|
||||
<strong>{box.name}</strong>
|
||||
<span class="hint">{box.username} · {box.host}</span>
|
||||
</div>
|
||||
<button class="ghost" onclick={() => syncMailbox(box)} disabled={syncingId === box.id}>
|
||||
{syncingId === box.id ? t('int.fetching') : t('int.fetchNow')}
|
||||
</button>
|
||||
<button class="ghost remove" onclick={() => removeMailbox(box)}>
|
||||
{t('int.disconnect')}
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
|
||||
{#if addingMailbox}
|
||||
<form class="add-mailbox" onsubmit={addMailbox}>
|
||||
<div class="field">
|
||||
<label for="mb-host">{t('int.host')}</label>
|
||||
<input id="mb-host" bind:value={newMailbox.host} placeholder="imap.mail.me.com:993" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="mb-user">{t('int.username')}</label>
|
||||
<input id="mb-user" bind:value={newMailbox.username} autocomplete="off" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="mb-pass">{t('int.appPassword')}</label>
|
||||
<input id="mb-pass" type="password" bind:value={newMailbox.password} autocomplete="off" />
|
||||
<p class="hint">{t('int.appPasswordHint')}</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<button type="submit" class="primary" disabled={connecting}>
|
||||
{connecting ? t('int.connecting') : t('int.connect')}
|
||||
</button>
|
||||
<button type="button" class="ghost" onclick={() => (addingMailbox = false)}>
|
||||
{t('account.cancel')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{:else}
|
||||
<div class="row">
|
||||
<button class="ghost" onclick={() => (addingMailbox = true)}>{t('int.addMailbox')}</button>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
/* Folded away once it is set: it is a one-off registration, and a panel that
|
||||
keeps showing its own setup instructions after setup reads as unfinished. */
|
||||
|
||||
Reference in New Issue
Block a user