From 45c8929697cc5406ce1d46d2887ad9c2ea0b2abf Mon Sep 17 00:00:00 2001 From: John Coffey Date: Sat, 5 Sep 2026 01:11:25 -0700 Subject: [PATCH] Withdraw the key manager, and keep what probing it established A Settings section for public keys is furniture, not a feature. Nothing in ihasmail signs, encrypts, decrypts or verifies with a key, so the page could only ever tell the reader in its own footnote that adding one does nothing. It is withdrawn on that reasoning -- the same reasoning that closed PR #67, reached again with the code in front of us. So this reverts every user-visible part of it: the section, the lib, the mock handlers, the component and the 261 catalogue strings. Nothing in web/ or server/ differs from main now. What stays is the part that was expensive and is true regardless. The x:PublicKey registry was probed against a live 0.16.20 on 2026-09-05, and the findings are now in KNOWN-ISSUES rather than in a closed pull request -- which is where they sat for the nine days between #67 and this branch, and why the work was done twice. Consolidated into one entry, framed as what Stalwart does rather than what ihasmail offers: - an ordinary user may read and write their own keys, whatever the permissions table says - the registry takes S/MIME certificates as well as OpenPGP keys, and parses both -- confirmed with a real self-signed X.509 certificate, and a malformed one gets its own BER decoding error - a key can parse and still be refused, with different words. A sign-and-certify key -- what `gpg --quick-generate-key` makes -- gets "Could not find any suitable keys", which is not a paste error and must not be shown as one - emailAddresses comes back as {} when empty, an object where a list property should be an array. It type-checks, then throws in join() - a create answers with the id alone; patching `key` is allowed - expiresAt is the registry's field and is not derived from the key ROADMAP now says plainly that key management has been built and withdrawn twice, that the registry is not the obstacle, and that verifying a signature -- which needs only public keys -- is the shortest route to a key being worth having. Encryption at rest moves from "not offered yet" to refused: it is a one-way door, since turning it off does not decrypt what is already there, and that is not a switch to hand an ordinary user however easy it would be to add. --- FEATURES.md | 69 ------ KNOWN-ISSUES.md | 15 +- README.md | 1 - ROADMAP.md | 8 +- server/src/mock/index.ts | 116 ---------- web/src/lib/__tests__/publicKeys.test.ts | 103 --------- web/src/lib/publicKeys.ts | 172 -------------- web/src/locales/de.ts | 30 --- web/src/locales/es.ts | 30 --- web/src/locales/fr.ts | 30 --- web/src/locales/ja.ts | 30 --- web/src/locales/nl.ts | 30 --- web/src/locales/pt-BR.ts | 30 --- web/src/locales/ru.ts | 30 --- web/src/locales/uk.ts | 30 --- web/src/locales/zh-Hans.ts | 30 --- web/src/views/settings/KeysSettings.tsx | 212 ------------------ web/src/views/settings/SettingsView.tsx | 4 +- .../settings/__tests__/keys-settings.test.tsx | 145 ------------ 19 files changed, 14 insertions(+), 1101 deletions(-) delete mode 100644 web/src/lib/__tests__/publicKeys.test.ts delete mode 100644 web/src/lib/publicKeys.ts delete mode 100644 web/src/views/settings/KeysSettings.tsx delete mode 100644 web/src/views/settings/__tests__/keys-settings.test.tsx diff --git a/FEATURES.md b/FEATURES.md index ea2ba81..a5403ae 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -1212,75 +1212,6 @@ Over Stalwart's own registry objects, so there is no administrator in the loop: credentials". Doing it properly means implementing OAuth; that is in [ROADMAP.md](ROADMAP.md). -## Encryption keys - -Settings › Encryption keys publishes OpenPGP public keys and S/MIME certificates -on the account, over Stalwart's `x:PublicKey` registry: list, add, rename, -remove. Only public material — no private key is stored, requested or sent by -any of it. - -Everything below was established against a live Stalwart **0.16.20** on -2026-09-05, rather than taken from the documentation, which is wrong on the -first two: - -- **An ordinary user may read *and* write their own keys.** Stalwart's - permissions table lists every `sysPublicKey*` permission as administrative. - The server granted them to a normal account: `x:PublicKey/get` and - `x:PublicKey/query` both answered, and a create carrying a deliberately - malformed key came back `invalidProperties` naming `key` — a rejection of the - key, not of the person. Had the documentation been right this section would - have been useless to everyone but an administrator. -- **Stalwart parses the key itself** and says precisely what is wrong with it: - *"Failed to decode OpenPGP public key: Malformed packet: Malformed CTB: MSB of - ptag not set."* So ihasmail does not validate key material. Anything it - checked would be a second opinion, and the one that counts would still be the - server's — its message is shown verbatim, the way password-policy rejections - already are. -- **Certificates are parsed too, by their own decoder.** The registry is not - OpenPGP-only, though every other message it returns reads as though it were: a - self-signed X.509 certificate registers, reads back and removes cleanly, and a - malformed one is refused with *"Failed to decode X509 certificate: BER - decoding error…"*. So the S/MIME half of this section is real, and the server - is the thing checking it in both formats. -- **A key that parses can still be refused, and says something else.** A - sign-and-certify key with no encryption subkey — which is what - `gpg --quick-generate-key` produces by default — comes back *"Could not find - any suitable keys in OpenPGP public key"*. That is the rejection somebody - exporting from GnuPG is most likely to meet, and it is not a paste error: - the fix is to add an encryption subkey, not to paste more carefully. Showing - the server's own two sentences distinguishes the two; showing "invalid key" - would not. -- **`emailAddresses` arrives as `{}` when it is empty** — an object, where a - JMAP list property should be an array. ihasmail checks the shape rather than - trusting the type, so nothing puts an object through `join()` halfway through - rendering the list. -- **Creating a key answers with its id and nothing else**, so adding one - reloads the list instead of believing what came back. Renaming and removing - both work; removal leaves the registry genuinely empty. - -**`expiresAt` is the registry's field, not the key's.** A certificate valid for -a year registers with no expiry set, and the card says so. Reading the real one -means parsing the certificate, which is the second opinion this section exists -not to offer. - -The kind badge — OpenPGP or S/MIME — is read from the armour header alone, which -is a label rather than a parse, and the excerpt beside each key is deliberately -not called a fingerprint: computing a real one means parsing the key, and naming -it a fingerprint would invite somebody to verify against it. - -**What this does not do.** Nothing signs, encrypts, decrypts or verifies with -these keys. The page says so. Adding one does not by itself start encrypting -your mail — the one thing known to consume a registered key is -`encryptionAtRest`, a field on `x:AccountSettings` beside `description`, -`locale` and `timeZone`. There is no `x:EncryptionAtRest` object whatever the -documentation suggests — asking for one on 0.16.20 is an `unknownMethod` — and -the field's value is a typed object, `{"@type": "Disabled"}`, not a bare string. -ihasmail does not yet offer it, and it carries a caveat of its own: turning it -off does not decrypt what is already there. Signing and decrypting need a private key in a page -served by the same host that would handle it, which is a security model to agree -before it is a feature to build. Verifying a signature needs only public keys -and is tractable on its own. See [ROADMAP.md](ROADMAP.md). - ## Privacy by default Remote images blocked, the proxy on, read receipts never automatic, no diff --git a/KNOWN-ISSUES.md b/KNOWN-ISSUES.md index 794ebe2..e4ead2d 100644 --- a/KNOWN-ISSUES.md +++ b/KNOWN-ISSUES.md @@ -39,12 +39,15 @@ works the same way — and dropped where 0.15 was the whole subject. Support for - **Address book sharing works, and was briefly withdrawn by mistake.** It was taken out alongside mail folders on 2026-08-27 on a report that it behaved the same way; the report was mistaken and the feature was put back the same day. Nothing was ever shown to be wrong with it, and Stalwart documents address books as shareable. Recorded because the withdrawal is in the history and would otherwise read as a finding. Shared books now appear in the Contacts pane under "Shared with me" rather than behind an account switch, and their contacts are offered when addressing a message. - **Stalwart lets a sharee subscribe to a shared calendar but not a shared address book.** Subscribing is a write to the *owner's* account -- `isSubscribed` lives on the collection, not on the reader -- and 0.16.19 refuses it for a book shared read-only: `AddressBook/set` answers successfully with the id in `notUpdated`, `forbidden`, *"You are not allowed to modify this address book."* The identical `Calendar/set` on a shared calendar is accepted. **Confirmed live on 0.16.19 (2026-08-27)** from a second account holding both shares, which is the only place it shows: from the owner's own account the write succeeds and everything looks fine. So ihasmail asks the server first, because a preference the server holds is one every client agrees about, and keeps the answer in its own synced settings (`addedShares`) when the server will not. Two things this cost, both worth remembering: the refusal arrives as a *successful* response, so the code that ignored `notUpdated` saw nothing wrong and the button simply did nothing; and it is invisible from the owner's account, so it took two browsers signed in as two accounts to find at all. The mock now refuses the same write for the same reason, since one that accepted it agreed with the belief that shipped. - **`shareWith` is not returned unless a client asks for it by name.** A `Calendar/get` or `AddressBook/get` with no `properties` comes back without the field at all — not null, not empty, absent — **confirmed live on 0.16.19 (2026-08-27)** against a calendar and an address book that were genuinely shared with another account: omit the list and there is no `shareWith`; name it and the sharee is right there. Every consequence was silent. Nothing was badged as shared, "Stop sharing" never appeared because nothing looked shared, and the share dialog opened on *"not shared with anyone yet"* over a live share — so the one screen that existed to manage sharing was the one most confidently wrong about it. Files never had this, because `fileNodeProps` had always named the property; calendars, address books and mail folders fetched everything and got less. Mail folders mattered in a way of their own: sharing one is withdrawn, and the only way to clear a share already made is a **Stop sharing** entry that appears when a folder looks shared — so without the property the escape hatch for the exact situation it was built for was invisible. The mock now omits it the same way, since one that hands it over unasked lets a client that never asks look correct everywhere except against a real server. -- **`x:PublicKey` is readable and writable by an ordinary user, whatever the permissions table says.** Stalwart documents every `sysPublicKey*` permission as administrative. **Confirmed live on 0.16.20 (2026-09-05)** from a normal account with no administrative rights: `x:PublicKey/get` and `x:PublicKey/query` both answered, a create with a malformed key was refused with `invalidProperties` naming `key` rather than `forbidden` — a rejection of the key, not of the person — and a create with a usable key, a rename, and a destroy all succeeded. Had the documentation been right, Settings › Encryption keys would have been useless to everybody but an administrator, which is why it was probed before it was built. -- **The registry takes S/MIME certificates as well as OpenPGP keys, and parses both.** **Confirmed live on 0.16.20 (2026-09-05)** with a self-signed X.509 certificate carrying `emailProtection` and an `email:` SAN: registered, read back, and destroyed. A malformed one is refused with a decoder of its own — *"Failed to decode X509 certificate: BER decoding error: Expected Tag { class: Universal, value: 16 } tag…"* — which is what makes the S/MIME half of Settings › Encryption keys real rather than decorative. It was worth checking rather than assuming, because every *other* message the registry returns names OpenPGP, including for input that is not OpenPGP at all, so the server reads as though OpenPGP were the only format it knows. It is not. -- **`expiresAt` is a registry field and is not derived from the key.** A certificate valid for a year registers with `expiresAt: null`, so the card says "No expiry set" about a credential that does expire. **Confirmed live on 0.16.20 (2026-09-05).** ihasmail leaves it that way on purpose: reading the expiry means parsing the certificate, which is the second opinion this whole section refuses to offer — and a date ihasmail extracted would disagree with the server's own field the moment the two ever differed. What the row reports is what the registry holds. -- **A key can parse perfectly and still be refused, for a different reason.** `gpg --quick-generate-key` makes a sign-and-certify key; exporting that and registering it comes back *"Could not find any suitable keys in OpenPGP public key"*, distinct from the parser's *"Failed to decode OpenPGP public key: Malformed packet: Malformed CTB…"*. **Confirmed live on 0.16.20 (2026-09-05)**, both wordings. The distinction matters to whoever pasted it: one says paste it again, the other says the key needs an encryption subkey and no amount of care with the clipboard will help. ihasmail shows the server's sentence verbatim rather than reducing both to "invalid key". The mock reproduces both, the second behind a `SIGNONLY` marker in the block, because a branch nothing can reach is a branch nobody has seen. -- **`emailAddresses` on a public key comes back as `{}` rather than `[]`.** An object where a JMAP list property should be an array, and only when it is empty. **Confirmed live on 0.16.20 (2026-09-05)** on a key registered without addresses. Nothing fails loudly: it is a plain `Get` response that type-checks against a hand-written interface and then throws in `join()` while the list is rendering. ihasmail checks the shape rather than trusting the type, the mock answers `{}` the same way, and a test pins it — a mock that helpfully returned `[]` would let that crash ship. -- **Creating a public key answers with the id alone.** No `createdAt` in the `created` object, so a client that read one back out of the create response would show `undefined` for the date it was added. **Confirmed live on 0.16.20 (2026-09-05).** ihasmail reloads the list after adding rather than believing the response. Patching `key` on an existing entry, incidentally, *is* allowed by the server; ihasmail does not do it and the mock refuses it, so that replacing a key stays add-then-remove and `createdAt` keeps meaning what it says. +- **Stalwart's `x:PublicKey` registry works, and ihasmail deliberately does not expose it.** A Settings section for it has been built twice — [PR #67](https://github.com/Coffey-Labs/ihasmail/pull/67), closed 2026-08-26, and [PR #285](https://github.com/Coffey-Labs/ihasmail/pull/285) — and withdrawn both times, for a reason that has nothing to do with the server: **nothing in ihasmail signs, encrypts, decrypts or verifies with a key**, so a page for managing them is furniture rather than a feature. It ends up telling the reader, in its own footnote, that adding a key does nothing. The registry is written up here rather than in [ROADMAP.md](ROADMAP.md) because what follows is established fact about Stalwart that cost a live probe, and losing it twice to a closed pull request was how the second attempt came to exist at all. Everything below was **confirmed live on 0.16.20 (2026-09-05)** from a normal account with no administrative rights, and the full round trip — create, read back, rename, patch, destroy — succeeded for both formats. + + - **An ordinary user may read *and* write their own keys**, whatever the permissions table says: Stalwart documents every `sysPublicKey*` permission as administrative, and the server granted them anyway. A create carrying a malformed key was refused with `invalidProperties` naming `key` rather than `forbidden` — a rejection of the key, not of the person. Had the documentation been right, any such feature would have been useless to everybody but an administrator, which is why this was probed first. + - **It takes S/MIME certificates as well as OpenPGP keys, and parses both.** A self-signed X.509 certificate carrying `emailProtection` and an `email:` SAN registered, read back and destroyed cleanly, and a malformed one is refused by a decoder of its own: *"Failed to decode X509 certificate: BER decoding error: Expected Tag { class: Universal, value: 16 } tag…"*. Worth checking rather than assuming, because every *other* message the registry returns names OpenPGP — including for input that is not OpenPGP at all — so the server reads as though OpenPGP were the only format it knows. It is not. + - **A key can parse perfectly and still be refused, and says something different when it is.** A sign-and-certify OpenPGP key with no encryption subkey — which is what `gpg --quick-generate-key` produces — comes back *"Could not find any suitable keys in OpenPGP public key"*, distinct from the parser's *"Failed to decode OpenPGP public key: Malformed packet: Malformed CTB…"*. Any client showing these must keep them apart: one says paste it again, the other says the key needs an encryption subkey and no amount of care with the clipboard will help. Certificates have no equivalent trap, since one issued for email use has key encipherment by construction. + - **`emailAddresses` comes back as `{}` when empty** — an object, where a JMAP list property should be an array. Nothing fails loudly: it is a plain `Get` response that type-checks against a hand-written interface and then throws in `join()` while a list renders. A client must check the shape rather than trust the type. + - **A create answers with the id alone**, no `createdAt`, so anything that reads the date back out of the create response gets `undefined`. **Patching `key` on an existing entry is allowed**, which is worth knowing and probably worth not doing: replacing a key by adding one and removing the old keeps `createdAt` meaning what it says. + - **`expiresAt` is the registry's own field and is not derived from the key.** A certificate valid for a year registers with `expiresAt: null`. Reading the real date means parsing the certificate, and a date a client extracted would disagree with the server's field the moment the two ever differed. + - **Read receipts are built here, not by the server** — JMAP has an extension for them, [RFC 9007](https://www.rfc-editor.org/rfc/rfc9007.html)'s `MDN/send`, and Stalwart does not implement it: `urn:ietf:params:jmap:mdn` is not among its capabilities. So ihasmail assembles the `multipart/report` itself and sends it the long way round — raw MIME uploaded as a blob, `Email/import`, then `EmailSubmission` — which is also why the receipt lands in Sent, where it honestly belongs. Non-ASCII parts are base64 rather than `8bit`, so nothing depends on 8BITMIME surviving every hop. There is deliberately no "always send" setting: a receipt confirms to whoever asked that the address is live and when it was read, to an address of the sender's choosing, so each one is a decision. Verified against the mock end to end (upload, import, submit, `$mdnsent`), and **confirmed live on 0.16.19 (2026-08-26)**: a receipt asked for by a real sender was assembled, uploaded, imported and submitted, landed in Sent, and set `$mdnsent` so a second look does not offer to send another. - **Where 0.16 advertises `urn:stalwart:jmap`** — not where a JMAP client would look, and this now decides whether a sign-in is allowed at all. Stalwart builds the session-level `capabilities` from a fixed list (`Session::new`, plus WebSocket) that has never contained this capability, in any 0.16.x from 0.16.0 to 0.16.19. It hands it out per-account instead, so it appears in `primaryAccounts` and in each account's `accountCapabilities`. ihasmail tested for it in `capabilities` alone, which made every real 0.16 server read as older than 0.16 — and that one check drove three things: self-service credentials fell back to `POST /api/account/auth`, which 0.16 removed, so password changes, 2FA and app passwords all failed with "this mail server does not offer self-service credential management"; About reported the wrong generation; and Files took the older code path. It now looks in all three places, and is covered by tests on each. Worth restating plainly, because the stakes went up when 0.15 support was dropped: there is no longer a fallback path for this check to be wrong *into*. Getting it wrong now refuses every sign-in against a perfectly good server — a loud failure rather than a quiet misrouting, which is the trade the removal was making. - **HTML signatures** — Stalwart caps a signature at 2047 **bytes** (`value.len() < 2048` on a Rust string, so UTF-8 bytes, not characters). ihasmail compacts pasted HTML, moves images to Files and, if still too large, keeps the full signature in Files behind a short marker; other clients see a text fallback. Confirmed live on 0.15.5 (2026-08-24): oversized, non-ASCII and inline-image signatures all save, and a test message arrived intact at Gmail with the logo inline. diff --git a/README.md b/README.md index 280db97..f656512 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,6 @@ More, including the mobile layout, on [ihasmail.org](https://ihasmail.org/#scree - **Calendar** — JMAP Calendars / JSCalendar: month/week/day/agenda, recurrence, attendees and free-busy, colour categories - **Contacts** — JMAP Contacts / JSContact: address books, groups, full editor, vCard import/export - **Files** — JMAP FileNode: browse, upload, download, rename, move, delete -- **Encryption keys** — publish OpenPGP public keys and S/MIME certificates on the account over Stalwart's `x:PublicKey` registry. Public material only: no private key is stored, requested or sent. Managing them is all it does — nothing signs, encrypts, decrypts or verifies yet, and the page says so - **Settings that follow the account**, not the browser — kept in a `settings.json` in the account's own JMAP Files, so ihasmail itself stays stateless - **Runs read-only** — one optional write path, and with it switched off the container needs no volume and no writable root. `IMMUTABLE=1` is checked at startup rather than trusted, so a half-applied switch refuses to boot instead of failing quietly. See [Running immutably](#running-immutably) - **Nine new interface languages** — German, Spanish, French, Dutch, Portuguese (Brazil), Russian, Ukrainian, Simplified Chinese and Japanese, alongside English and separate from the date-and-time locale. Every one is marked **Beta**: they were made by AI and no native speaker has read them yet, which Settings says plainly, with a link for reporting anything wrong diff --git a/ROADMAP.md b/ROADMAP.md index fd8d82f..c9dff47 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -15,13 +15,13 @@ See [KNOWN-ISSUES.md](KNOWN-ISSUES.md) for what is built but worth knowing about - **A translation anybody has checked.** The translations themselves shipped on 2026-08-31 and are no longer on this page: nine of them, alongside English, and the extraction that had always been the hard half is done — see [FEATURES.md](FEATURES.md#interface-language). What is *not* done is the other half, and it is the half that cannot be bought or automated. All nine were produced by AI against standard dictionaries and **not one has been read by anybody who speaks the language**, which is exactly where a bad translation does harm rather than merely looking untidy. They ship marked Beta, with that said in Settings and a link for reporting anything wrong, because shipping them quietly would ask people to trust text nobody has checked. A language loses the Beta mark when a speaker reads it and says so — a deliberate act by a person, not something a coverage percentage earns. If you speak one of them and are willing to read a few hundred strings, that is the single most useful thing anyone could contribute right now. - **Right-to-left languages.** Arabic, Hebrew and Persian are held back deliberately, and not for want of translators. RTL is bidi and layout work throughout — mirrored panes, gesture directions, icon sides, the message list's own geometry — and a catalogue without it produces a page that is translated and unusable. Adding one is not another entry in the picker. - **Two-factor sign-in.** Today an account with 2FA must use an app password (see [Quick start](README.md#quick-start-docker)), and Settings › Security offers no way to switch 2FA *on* — only off, for an account that already has it. Supporting a TOTP code directly means implementing OAuth: Stalwart offers the authorization-code and device flows and no password grant, so ihasmail would hand sign-in to Stalwart's own login and come back with a token. That is a better security posture than the sealed password it holds now — a refresh token rather than a credential — but it replaces ihasmail's own sign-in page for those users and may need an OAuth client registered. Came out of [#75](https://github.com/Coffey-Labs/ihasmail/issues/75), which is closed: what was reported there was a sign-in refused with nothing but "Invalid credentials", and that was fixed by saying what is actually happening and pointing at app passwords. The OAuth work it uncovered is tracked here rather than as an open issue, so there is no ticket to watch for it. -- **Signing, encrypting, decrypting and verifying mail.** *Managing* keys is built and no longer on this page: Settings › Encryption keys publishes OpenPGP public keys and S/MIME certificates over Stalwart's `x:PublicKey` registry — see [FEATURES.md](FEATURES.md#encryption-keys). What is not built is anything that *uses* them, and the reason is not Stalwart. S/MIME itself is CMS over the message body: JMAP hands a client the MIME blob and the rest is the client's work. +- **Signed and encrypted mail — and, for now, managing the keys for it.** Nothing signs, encrypts, decrypts or verifies, and the reason is not Stalwart. S/MIME and OpenPGP are both work the *client* does over the message body: JMAP hands it the MIME blob and the rest is ours. - The blocker is a security model, not code. Signing and decrypting need a private key in a page served by the same host that would handle it, which runs straight into two things ihasmail says about itself: that it never stores a credential, and that it runs immutably with nowhere to keep one. That question has to be answered out loud before it is a feature to build, and it has not been. Verifying a signature is the exception — it needs only public keys, touches none of this, and is tractable on its own. + The blocker is a security model, not code. Signing and decrypting need a private key in a page served by the same host that would handle it, which runs straight into two things ihasmail says about itself: that it never stores a credential, and that it runs immutably with nowhere to keep one. That question has to be answered out loud before it is a feature to build, and it has not been. **Verifying a signature is the exception** — it needs only public keys, touches none of this, and is the one piece that could make a key worth having. - Encryption at rest is the one thing known to consume a registered key, and ihasmail does not offer it yet. It is `encryptionAtRest`, a field on `x:AccountSettings` beside `description`, `locale` and `timeZone` — there is no `x:EncryptionAtRest` object whatever the docs suggest, and its value is a typed object (`{"@type": "Disabled"}`) rather than a bare string. It is self-service with no administrator involved. **The design caveat that has to be got right before any encryption UI ships:** turning it *off does not decrypt what is already there*. Enabling it is a one-way door — every message delivered while it was on stays encrypted on disk, readable only by a client holding the private key. That is a property of Stalwart rather than a policy choice, and it is the part a user cannot undo by changing their mind. + *Managing* keys is a smaller thing and has been built twice — [PR #67](https://github.com/Coffey-Labs/ihasmail/pull/67) and [PR #285](https://github.com/Coffey-Labs/ihasmail/pull/285) — and withdrawn twice, on the same reasoning both times: a Settings section for keys that nothing uses is furniture, not a feature, and it ends up saying so in its own footnote. Stalwart's `x:PublicKey` registry is not the obstacle. It works, it holds OpenPGP keys and S/MIME certificates alike, and it is fully probed — see [KNOWN-ISSUES.md](KNOWN-ISSUES.md), which is where those findings live precisely so that the third attempt does not have to rediscover them. Key management ships when something consumes a key, and verifying an incoming signature is the shortest route to that. - All of the above was probed against a live Stalwart **0.16.20** on 2026-09-05; the findings, including two rejections worth telling apart and a list property that arrives as an object, are in [KNOWN-ISSUES.md](KNOWN-ISSUES.md). + **Encryption at rest is not the route, and is refused rather than deferred.** Stalwart offers it as `encryptionAtRest`, a field on `x:AccountSettings` beside `description`, `locale` and `timeZone` — there is no `x:EncryptionAtRest` object whatever the docs suggest, and its value is a typed object (`{"@type": "Disabled"}`) rather than a bare string. It is self-service, needs no administrator, and would be easy to offer. It will not be: turning it *off does not decrypt what is already there*. Every message delivered while it was on stays encrypted on disk, readable only by a client holding the private key, so switching it on is a one-way door — and a toggle that reads as "make my mail safer" while quietly being irreversible is the wrong thing to hand an ordinary user. That is a property of Stalwart rather than a policy choice, which is exactly why the answer here is a policy choice. **Why S/MIME rather than OpenPGP, and why neither is urgent.** End-to-end encrypted mail never reached the mainstream and is not on its way there: as a share of the world's email, PGP-encrypted messages are a rounding error, and the most successful use of OpenPGP is signing packages rather than sending mail. The reasons are structural rather than a matter of better tooling. Everyone in a thread has to take part, so the network effect works against it from the first reply. Key discovery was never solved — keyservers were unauthenticated and got weaponised in the 2019 certificate-flooding attacks, which made specific people's keys unusable by any client that fetched them, and WKD is better without being universal. There is no forward secrecy, so one compromised key retroactively opens everything ever received. The metadata stays in the clear: subject lines are cleartext in classic PGP/MIME, and who corresponded with whom is often the sensitive part. Losing a key loses the mail permanently. And it breaks the client — no server-side search, degraded spam filtering, awkward on a phone — while EFAIL showed in 2018 that the clients themselves were exploitable through MIME and HTML handling. Meanwhile the actual privacy win arrived invisibly and without anyone participating, in STARTTLS, MTA-STS and DANE. diff --git a/server/src/mock/index.ts b/server/src/mock/index.ts index eca2cef..f69de92 100644 --- a/server/src/mock/index.ts +++ b/server/src/mock/index.ts @@ -66,9 +66,6 @@ const nextState = () => String(state.n++); /** Push subscriptions, as a fresh account has none. */ const pushSubscriptions: Obj[] = []; -/** Registered public keys. Empty to start, like a fresh account. */ -const publicKeys: Obj[] = []; - const mailboxes: Obj[] = [ mb("inbox", "Inbox", "inbox"), mb("drafts", "Drafts", "drafts"), @@ -978,82 +975,6 @@ const handlers: Record = { } return setResp({ created, notCreated, updated, notUpdated, destroyed }); }, - /* - * Public keys, as 0.16.20 actually behaves -- established against a live - * server on 2026-09-05, because the documentation disagrees on the first two: - * - * - an ordinary user may read AND write their own keys. The docs list the - * sysPublicKey* permissions as administrative; the server granted them. - * - * - the server parses the key. A malformed one comes back invalidProperties - * naming `key`, with the parser's own complaint in the description -- - * not a bland "invalid". A mock that took any string would let a client - * ship without ever handling the rejection, which is the shape of every - * bug this mock has been taught to reproduce since. - * - * - a well-formed key with nothing to encrypt to is refused just as - * firmly, and says something different: "Could not find any suitable - * keys in OpenPGP public key". A sign-only key parses perfectly and is - * still no use to a server whose reason for holding one is encryption. - * This is the rejection somebody exporting from GnuPG will actually - * meet, so the mock has to be able to produce it. - * - * - `created` carries the id and nothing else -- no createdAt. A client - * that read one back out of the create response would get undefined, - * which is why adding a key reloads the list. - */ - "x:PublicKey/get": (a) => genericGet(publicKeys)(a), - "x:PublicKey/query": () => ({ accountId: ACCOUNT, queryState: String(state.n), canCalculateChanges: true, position: 0, ids: publicKeys.map((k) => k.id as string), total: publicKeys.length }), - "x:PublicKey/set": (a) => { - const created: Obj = {}; - const notCreated: Obj = {}; - const updated: Obj = {}; - const notUpdated: Obj = {}; - const destroyed: string[] = []; - for (const [cid, obj] of Object.entries((a.create as Obj) ?? {})) { - const o = obj as Obj; - const complaint = keyComplaint(String(o.key ?? "")); - if (complaint) { - notCreated[cid] = { type: "invalidProperties", properties: ["key"], description: complaint }; - continue; - } - const id = `pk${randomUUID().slice(0, 6)}`; - publicKeys.push({ - id, - key: o.key, - description: o.description ?? "", - createdAt: new Date().toISOString(), - expiresAt: o.expiresAt ?? null, - // An empty `emailAddresses` comes back from the real server as `{}` -- - // an object where a JMAP list property should be an array. The client - // survives it by checking rather than trusting, and it only survives - // because something reproduced it: a mock answering `[]` would have - // let `.join(", ")` ship and throw against a real server. - emailAddresses: Array.isArray(o.emailAddresses) && o.emailAddresses.length ? o.emailAddresses : {}, - }); - // Only the id: the live server sends no createdAt here. - created[cid] = { id }; - state.n++; - } - for (const [id, patch] of Object.entries((a.update as Obj) ?? {})) { - const k = publicKeys.find((x) => x.id === id); - if (!k) { notUpdated[id] = { type: "notFound" }; continue; } - // The live server ALLOWS this -- patching `key` on 0.16.20 answers - // `updated`. The mock refuses it anyway, and deliberately: ihasmail - // replaces a key by adding one and removing the old, which keeps - // createdAt meaning what it says, and a mock that permitted the patch - // would quietly bless a path the client is not supposed to take. - if ("key" in (patch as Obj)) { notUpdated[id] = { type: "invalidProperties", properties: ["key"], description: "Property cannot be changed." }; continue; } - Object.assign(k, patch); - updated[id] = null; - state.n++; - } - for (const id of (a.destroy as string[]) ?? []) { - const i = publicKeys.findIndex((x) => x.id === id); - if (i >= 0) { publicKeys.splice(i, 1); destroyed.push(id); state.n++; } - } - return setResp({ created, notCreated, updated, notUpdated, destroyed }); - }, "x:AppPassword/get": (a) => genericGet(account.appPasswords)(a), "x:AppPassword/set": (a) => { const created: Obj = {}; @@ -1272,43 +1193,6 @@ const handlers: Record = { }, }; -/** - * What Stalwart says when it will not take a key. Both wordings are the - * server's own, taken verbatim from a live 0.16.20 on 2026-09-05 -- a client - * that only ever saw "invalid key" would show something less useful than what - * the server was already offering. - * - * There are three, and they are worth keeping apart because they are different - * problems. A block that will not parse is usually a bad copy and paste. A - * block that parses and is still refused is a key that cannot encrypt -- - * `gpg --quick-generate-key` makes a sign-and-certify key by default, and - * exporting that gets you "Could not find any suitable keys" however carefully - * it was pasted. And a certificate gets its own decoder and its own complaint: - * Stalwart parses X.509 as seriously as it parses OpenPGP, which is the thing - * that makes the S/MIME half of this section real rather than decorative. - */ -function keyComplaint(key: string): string | null { - const k = key.trim(); - if (!k) return "Failed to decode OpenPGP public key: no key data."; - const pgp = k.startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----") && k.includes("-----END PGP PUBLIC KEY BLOCK-----"); - const x509 = k.startsWith("-----BEGIN CERTIFICATE-----") && k.includes("-----END CERTIFICATE-----"); - if (!pgp && !x509) return "Failed to decode OpenPGP public key: Malformed packet: Malformed CTB: MSB of ptag not set."; - const body = k.split(/\r?\n/).filter((l) => l && !l.startsWith("-----") && !l.startsWith("=") && !l.includes(":")).join(""); - // Enough base64 to be a key rather than a placeholder; the real parsers are - // stricter still, which is the point of surfacing their message and not ours. - if (body.length < 64) { - return x509 - ? "Failed to decode X509 certificate: BER decoding error: Expected Tag { class: Universal, value: 16 } tag, actual tag: Tag { class: Application, value: 14 } (Codec: BER)" - : "Failed to decode OpenPGP public key: Malformed packet: unexpected EOF."; - } - // The mock cannot read a key, so it cannot tell whether one can encrypt. - // A "SIGNONLY" marker anywhere in the block stands in for that, which is - // crude but reachable: the branch has to be reachable from the UI, or - // nobody will ever see the message it exists to return. - if (pgp && k.includes("SIGNONLY")) return "Could not find any suitable keys in OpenPGP public key"; - return null; -} - /* ---------- http ---------- */ function unauthorized(res: ServerResponse) { res.writeHead(401, { "content-type": "application/json", "www-authenticate": 'Basic realm="mock"' }); diff --git a/web/src/lib/__tests__/publicKeys.test.ts b/web/src/lib/__tests__/publicKeys.test.ts deleted file mode 100644 index 7ab03c6..0000000 --- a/web/src/lib/__tests__/publicKeys.test.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; - -const call = vi.fn(); -vi.mock("@/jmap/client", async (orig) => { - const real = await orig(); - return { ...real, client: { ...real.client, call: (...a: unknown[]) => call(...a), hasCapabilityAnywhere: () => true } }; -}); -vi.mock("@/store/session", () => ({ - useSession: { getState: () => ({ session: { primaryAccounts: { "urn:stalwart:jmap": "v" } }, accountFor: () => "v" }) }, -})); - -const { isExpired, keyExcerpt, keyKind, keyKindLabel, listPublicKeys } = await import("@/lib/publicKeys"); - -/** - * These read a key without parsing one. Stalwart parses it — with a real - * OpenPGP implementation that says precisely what is wrong — so anything - * checked here could only be a second opinion, and the one that counts would - * still be the server's. What is left is labelling: which sort of key this is, - * whether its stated expiry has passed, and enough of the body to tell two - * keys apart in a list. - */ - -const PGP = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBGAbCdEFGh\nijKLmnOPqrSt\n=aBc1\n-----END PGP PUBLIC KEY BLOCK-----"; -const X509 = "-----BEGIN CERTIFICATE-----\nMIIDdzCCAl+gAwIBAgIE\n-----END CERTIFICATE-----"; - -describe("which sort of key this is", () => { - it("reads the armour header, and only the header", () => { - expect(keyKind(PGP)).toBe("openpgp"); - expect(keyKind(X509)).toBe("smime"); - }); - - it("tolerates leading whitespace from a paste", () => { - expect(keyKind("\n\n " + PGP)).toBe("openpgp"); - }); - - it("says so rather than guessing when the header is not one it knows", () => { - // Not "invalid" — that is the server's call to make, not this function's. - expect(keyKind("ssh-ed25519 AAAAC3Nz")).toBe("unknown"); - expect(keyKind("")).toBe("unknown"); - expect(keyKindLabel(keyKind("nonsense"))).toBe("Unrecognised"); - }); -}); - -describe("expiry", () => { - const now = new Date("2026-08-26T12:00:00Z"); - - it("is not expired when no expiry was set", () => { - expect(isExpired({ expiresAt: null }, now)).toBe(false); - }); - - it("compares against the given moment, not the machine clock", () => { - expect(isExpired({ expiresAt: "2026-08-25T12:00:00Z" }, now)).toBe(true); - expect(isExpired({ expiresAt: "2026-08-27T12:00:00Z" }, now)).toBe(false); - }); - - it("treats an unreadable date as no expiry rather than as expired", () => { - // Marking a usable key "Expired" over a date we could not read would be - // worse than saying nothing about it. - expect(isExpired({ expiresAt: "whenever" }, now)).toBe(false); - }); -}); - -describe("telling two keys apart", () => { - it("excerpts the body, skipping armour, headers and the checksum", () => { - const x = keyExcerpt(PGP, 12); - expect(x).toBe("mQINBGAbCdEF"); - expect(x).not.toContain("-----"); - expect(x).not.toContain("=aBc1"); - }); - - it("gives something rather than nothing for a key with no body", () => { - expect(keyExcerpt("-----BEGIN PGP PUBLIC KEY BLOCK-----\n-----END PGP PUBLIC KEY BLOCK-----")).toBe("—"); - }); -}); - -describe("reading the registry back", () => { - /* - * Stalwart answers an empty `emailAddresses` with `{}` -- an object, where a - * JMAP list property should be an array. Confirmed against a live 0.16.20 on - * 2026-09-05. Trusting the type would put an object through `.join(", ")` - * and throw in the middle of rendering the list, so the shape is checked - * rather than believed, and this is the test that says why. - */ - it("survives emailAddresses arriving as an object instead of an array", async () => { - call.mockResolvedValueOnce({ list: [{ id: "k1", key: PGP, description: "Work", createdAt: "2026-09-05T07:48:03Z", expiresAt: null, emailAddresses: {} }] }); - const [k] = await listPublicKeys(); - expect(Array.isArray(k!.emailAddresses)).toBe(true); - expect(k!.emailAddresses).toEqual([]); - }); - - it("keeps the addresses when the server does send a list", async () => { - call.mockResolvedValueOnce({ list: [{ id: "k1", key: PGP, emailAddresses: ["a@example.com", 7, "b@example.com"] }] }); - const [k] = await listPublicKeys(); - // The stray number is dropped rather than rendered as "7". - expect(k!.emailAddresses).toEqual(["a@example.com", "b@example.com"]); - }); - - it("fills in what a sparse object leaves out, so the card never renders undefined", async () => { - call.mockResolvedValueOnce({ list: [{ id: "k1" }] }); - const [k] = await listPublicKeys(); - expect(k).toEqual({ id: "k1", key: "", description: "", createdAt: null, expiresAt: null, emailAddresses: [] }); - }); -}); diff --git a/web/src/lib/publicKeys.ts b/web/src/lib/publicKeys.ts deleted file mode 100644 index b6a680f..0000000 --- a/web/src/lib/publicKeys.ts +++ /dev/null @@ -1,172 +0,0 @@ -/** - * Public keys, over Stalwart's `x:PublicKey` registry. - * - * These are the keys other people use to encrypt mail *to* this account, and - * the ones a signature is checked against. Nothing secret is involved: no - * private key is held, asked for, or sent anywhere by any of this. - * - * Established against a live 0.16.20 on 2026-09-05 rather than assumed. The - * first two because the documentation says otherwise; the rest because they - * decide how this file has to be written: - * - * - An ordinary user may read *and* write their own keys. Stalwart's - * permissions table lists the `sysPublicKey*` permissions as - * administrative; the server granted them to a normal account. A create - * with a malformed key came back `invalidProperties`, not `forbidden`, - * which is a rejection of the key rather than of the person. - * - * - Stalwart parses the key itself, with a real OpenPGP implementation, and - * says precisely what is wrong: "Failed to decode OpenPGP public key: - * Malformed packet: Malformed CTB…". So ihasmail does not validate key - * material. Anything it checked would only be a second opinion, and the - * one that mattered would still be the server's. It refuses a *readable* - * key that cannot encrypt just as firmly, and differently — "Could not - * find any suitable keys in OpenPGP public key" — which is the rejection - * an exported sign-only key gets. - * - * - `emailAddresses` comes back as `{}` when it is empty: an object where a - * JMAP list property should be an array. `normalize` therefore checks the - * shape instead of trusting it, and every reader of this type gets a real - * array. Deleting that check reintroduces a crash in `.join()`. - * - * - A create answers with the id alone — no `createdAt` — so adding a key - * reloads the list rather than believing what it got back. - * - * - `destroy` works, and leaves the registry empty. That path had never been - * run against a real server before this probe. - */ -import { CAP, client, setErrorMessage } from "@/jmap/client"; -import type { GetResponse, Id, SetResponse } from "@/jmap/types"; -import { useSession } from "@/store/session"; -import { t } from "@/lib/i18n"; - -const STALWART = "urn:stalwart:jmap"; -const USING = [CAP.core, STALWART]; - -export interface PublicKey { - id: Id; - key: string; - description: string; - createdAt: string | null; - expiresAt: string | null; - emailAddresses: string[]; -} - -/** What a key can be edited to; `key` itself is replaced by adding a new one. */ -export type PublicKeyPatch = Partial>; - -const PROPS = ["id", "key", "description", "createdAt", "expiresAt", "emailAddresses"]; - -/** Whether this server offers the registry at all. */ -export function publicKeysAvailable(): boolean { - return client.hasCapabilityAnywhere(STALWART) && Boolean(accountId()); -} - -function accountId(): Id | null { - const s = useSession.getState(); - return s.session?.primaryAccounts?.[STALWART] ?? s.accountFor(CAP.mail); -} - -function normalize(raw: Partial & { id: Id }): PublicKey { - return { - id: raw.id, - key: typeof raw.key === "string" ? raw.key : "", - description: typeof raw.description === "string" ? raw.description : "", - createdAt: typeof raw.createdAt === "string" ? raw.createdAt : null, - expiresAt: typeof raw.expiresAt === "string" ? raw.expiresAt : null, - emailAddresses: Array.isArray(raw.emailAddresses) ? raw.emailAddresses.filter((a): a is string => typeof a === "string") : [], - }; -} - -export async function listPublicKeys(): Promise { - const id = accountId(); - if (!id) return []; - const res = await client.call>("x:PublicKey/get", { accountId: id, ids: null, properties: PROPS }, USING); - return res.list.map((k) => normalize(k as Partial & { id: Id })); -} - -export async function addPublicKey(key: string, description: string, extra: PublicKeyPatch = {}): Promise { - const id = accountId(); - if (!id) throw new Error(t("No account to add a key to.")); - const res = await client.call>( - "x:PublicKey/set", - { accountId: id, create: { k: { key: key.trim(), description: description.trim(), ...clean(extra) } } }, - USING, - ); - const err = res.notCreated?.k; - // The server's own words: it parsed the key and knows what is wrong with it. - if (err) throw new Error(setErrorMessage(err)); - return normalize((res.created?.k ?? { id: "" }) as Partial & { id: Id }); -} - -export async function updatePublicKey(keyId: Id, patch: PublicKeyPatch): Promise { - const id = accountId(); - if (!id) return; - const res = await client.call>("x:PublicKey/set", { accountId: id, update: { [keyId]: clean(patch) } }, USING); - const err = res.notUpdated?.[keyId]; - if (err) throw new Error(setErrorMessage(err)); -} - -export async function removePublicKey(keyId: Id): Promise { - const id = accountId(); - if (!id) return; - const res = await client.call>("x:PublicKey/set", { accountId: id, destroy: [keyId] }, USING); - const err = res.notDestroyed?.[keyId]; - if (err) throw new Error(setErrorMessage(err)); -} - -/** Drop keys the caller left undefined, so a patch never blanks a field by accident. */ -function clean(patch: PublicKeyPatch): Record { - const out: Record = {}; - for (const [k, v] of Object.entries(patch)) if (v !== undefined) out[k] = v; - return out; -} - -/* ------------------------------------------------------------------ */ -/* Reading a key without parsing one */ -/* ------------------------------------------------------------------ */ - -export type KeyKind = "openpgp" | "smime" | "unknown"; - -/** - * Which kind of key this is, from its armour header alone. - * - * Deliberately not a parse. The header is a label, and reading a label is not - * the same as validating the thing it is stuck to — the server does that, and - * a second opinion here could only ever disagree with the one that counts. - */ -export function keyKind(key: string): KeyKind { - const head = key.trimStart().slice(0, 120).toUpperCase(); - if (head.includes("BEGIN PGP PUBLIC KEY BLOCK")) return "openpgp"; - if (head.includes("BEGIN CERTIFICATE") || head.includes("BEGIN PKCS7")) return "smime"; - return "unknown"; -} - -/** - * "OpenPGP" and "S/MIME" are the formats' own names and stay as they are in - * every language; only the fallback is a word rather than a name, so only the - * fallback is translated. - */ -export function keyKindLabel(kind: KeyKind): string { - return kind === "openpgp" ? "OpenPGP" : kind === "smime" ? "S/MIME" : t("Unrecognised"); -} - -/** Whether a key has an expiry that has already passed. */ -export function isExpired(k: Pick, now = new Date()): boolean { - if (!k.expiresAt) return false; - const at = Date.parse(k.expiresAt); - return Number.isFinite(at) && at < now.getTime(); -} - -/** - * A short, stable excerpt of the key body, for telling two keys apart in a - * list. Not a fingerprint: computing a real one means parsing the key, and - * calling this a fingerprint would invite someone to verify against it. - */ -export function keyExcerpt(key: string, length = 24): string { - const body = key - .split(/\r?\n/) - .filter((l) => l && !l.startsWith("-----") && !l.includes(":") && !l.startsWith("=")) - .join(""); - return body.slice(0, length) || "—"; -} diff --git a/web/src/locales/de.ts b/web/src/locales/de.ts index b74c249..c647c04 100644 --- a/web/src/locales/de.ts +++ b/web/src/locales/de.ts @@ -1331,36 +1331,6 @@ export const catalog: Catalog = { "Empty “{name}”?": "„{name}“ leeren?", "Delete them": "Alle löschen", "Nothing was deleted": "Es wurde nichts gelöscht", - // ── Encryption keys, over Stalwart's x:PublicKey registry (#67) ── - "Encryption keys": "Verschlüsselungsschlüssel", - "Not available on this server": "Auf diesem Server nicht verfügbar", - "Public keys are kept in Stalwart's registry, which this server does not offer. ihasmail needs Stalwart 0.16 or newer for it.": "Öffentliche Schlüssel liegen in der Registry von Stalwart, die dieser Server nicht anbietet. ihasmail benötigt dafür Stalwart 0.16 oder neuer.", - "Public keys for this account — what other people encrypt to, and what a signature is checked against. These are public by nature: no private key is stored, requested, or sent by ihasmail.": "Öffentliche Schlüssel für dieses Konto — das, woran andere verschlüsseln und woran eine Signatur geprüft wird. Sie sind naturgemäß öffentlich: ihasmail speichert, erfragt und sendet keinen privaten Schlüssel.", - "No keys yet": "Noch keine Schlüssel", - "Add an OpenPGP public key or an S/MIME certificate to publish it on this account.": "Fügen Sie einen öffentlichen OpenPGP-Schlüssel oder ein S/MIME-Zertifikat hinzu, um es für dieses Konto zu veröffentlichen.", - "Key added": "Schlüssel hinzugefügt", - "Key removed": "Schlüssel entfernt", - "Remove “{name}”?": "„{name}“ entfernen?", - "this key": "diesen Schlüssel", - "Anyone holding it can still use it — removing it here only stops this account offering it.": "Wer ihn besitzt, kann ihn weiterhin verwenden — das Entfernen hier bewirkt nur, dass dieses Konto ihn nicht mehr anbietet.", - "Remove key": "Schlüssel entfernen", - "Key description": "Schlüsselbeschreibung", - "Untitled key": "Unbenannter Schlüssel", - "Click to rename": "Zum Umbenennen klicken", - "Expired": "Abgelaufen", - "Added": "Hinzugefügt", - "Addresses": "Adressen", - "No expiry set": "Kein Ablaufdatum festgelegt", - "Any address on this account": "Jede Adresse dieses Kontos", - "cryptography\u0004Key": "Schlüssel", - "Public key": "Öffentlicher Schlüssel", - "Work key": "Arbeitsschlüssel", - "Paste the whole armoured block, headers included. The server checks it and says what is wrong if it cannot read it.": "Fügen Sie den gesamten ASCII-Block samt Kopfzeilen ein. Der Server prüft ihn und sagt, was nicht stimmt, falls er ihn nicht lesen kann.", - "Add key": "Schlüssel hinzufügen", - "Add a key": "Einen Schlüssel hinzufügen", - "Stalwart stores these keys, and this release does no more than manage them: ihasmail does not yet sign, encrypt, decrypt or verify anything with them. Adding one does not by itself start encrypting your mail.": "Stalwart speichert diese Schlüssel, und diese Version verwaltet sie lediglich: ihasmail signiert, verschlüsselt, entschlüsselt und prüft damit noch nichts. Einen hinzuzufügen beginnt nicht von selbst, Ihre E-Mails zu verschlüsseln.", - "Unrecognised": "Nicht erkannt", - "No account to add a key to.": "Kein Konto vorhanden, zu dem ein Schlüssel hinzugefügt werden könnte.", }, plurals: { // ── Third pass ───────────────────────────────────────────────────── diff --git a/web/src/locales/es.ts b/web/src/locales/es.ts index 86efa53..f0ffcd1 100644 --- a/web/src/locales/es.ts +++ b/web/src/locales/es.ts @@ -1304,36 +1304,6 @@ export const catalog: Catalog = { "Empty “{name}”?": "¿Vaciar «{name}»?", "Delete them": "Eliminarlos", "Nothing was deleted": "No se ha eliminado nada", - // ── Encryption keys, over Stalwart's x:PublicKey registry (#67) ── - "Encryption keys": "Claves de cifrado", - "Not available on this server": "No disponible en este servidor", - "Public keys are kept in Stalwart's registry, which this server does not offer. ihasmail needs Stalwart 0.16 or newer for it.": "Las claves públicas se guardan en el registro de Stalwart, que este servidor no ofrece. ihasmail necesita Stalwart 0.16 o posterior para ello.", - "Public keys for this account — what other people encrypt to, and what a signature is checked against. These are public by nature: no private key is stored, requested, or sent by ihasmail.": "Claves públicas de esta cuenta: aquello con lo que otras personas cifran, y aquello con lo que se comprueba una firma. Son públicas por naturaleza: ihasmail no almacena, no solicita ni envía ninguna clave privada.", - "No keys yet": "Aún no hay claves", - "Add an OpenPGP public key or an S/MIME certificate to publish it on this account.": "Añada una clave pública OpenPGP o un certificado S/MIME para publicarlo en esta cuenta.", - "Key added": "Clave añadida", - "Key removed": "Clave quitada", - "Remove “{name}”?": "¿Quitar «{name}»?", - "this key": "esta clave", - "Anyone holding it can still use it — removing it here only stops this account offering it.": "Quien la tenga podrá seguir usándola: quitarla aquí solo hace que esta cuenta deje de ofrecerla.", - "Remove key": "Quitar clave", - "Key description": "Descripción de la clave", - "Untitled key": "Clave sin título", - "Click to rename": "Haga clic para cambiar el nombre", - "Expired": "Caducada", - "Added": "Añadida", - "Addresses": "Direcciones", - "No expiry set": "Sin caducidad establecida", - "Any address on this account": "Cualquier dirección de esta cuenta", - "cryptography\u0004Key": "Clave", - "Public key": "Clave pública", - "Work key": "Clave del trabajo", - "Paste the whole armoured block, headers included. The server checks it and says what is wrong if it cannot read it.": "Pegue el bloque blindado completo, cabeceras incluidas. El servidor lo comprueba y dice qué falla si no puede leerlo.", - "Add key": "Añadir clave", - "Add a key": "Añadir una clave", - "Stalwart stores these keys, and this release does no more than manage them: ihasmail does not yet sign, encrypt, decrypt or verify anything with them. Adding one does not by itself start encrypting your mail.": "Stalwart guarda estas claves y esta versión no hace más que gestionarlas: ihasmail todavía no firma, cifra, descifra ni verifica nada con ellas. Añadir una no empieza por sí sola a cifrar su correo.", - "Unrecognised": "No reconocida", - "No account to add a key to.": "No hay ninguna cuenta a la que añadir una clave.", }, plurals: { // ── Third pass ───────────────────────────────────────────────────── diff --git a/web/src/locales/fr.ts b/web/src/locales/fr.ts index 9592f76..c63778c 100644 --- a/web/src/locales/fr.ts +++ b/web/src/locales/fr.ts @@ -1309,36 +1309,6 @@ export const catalog: Catalog = { "Empty “{name}”?": "Vider « {name} » ?", "Delete them": "Les supprimer", "Nothing was deleted": "Rien n’a été supprimé", - // ── Encryption keys, over Stalwart's x:PublicKey registry (#67) ── - "Encryption keys": "Clés de chiffrement", - "Not available on this server": "Non disponible sur ce serveur", - "Public keys are kept in Stalwart's registry, which this server does not offer. ihasmail needs Stalwart 0.16 or newer for it.": "Les clés publiques sont conservées dans le registre de Stalwart, que ce serveur ne propose pas. ihasmail nécessite Stalwart 0.16 ou plus récent pour cela.", - "Public keys for this account — what other people encrypt to, and what a signature is checked against. These are public by nature: no private key is stored, requested, or sent by ihasmail.": "Clés publiques de ce compte — ce avec quoi les autres chiffrent, et ce sur quoi une signature est vérifiée. Elles sont publiques par nature : ihasmail ne stocke, ne demande ni n'envoie aucune clé privée.", - "No keys yet": "Aucune clé pour l'instant", - "Add an OpenPGP public key or an S/MIME certificate to publish it on this account.": "Ajoutez une clé publique OpenPGP ou un certificat S/MIME pour le publier sur ce compte.", - "Key added": "Clé ajoutée", - "Key removed": "Clé retirée", - "Remove “{name}”?": "Retirer « {name} » ?", - "this key": "cette clé", - "Anyone holding it can still use it — removing it here only stops this account offering it.": "Quiconque la détient peut toujours l'utiliser — la retirer ici empêche seulement ce compte de la proposer.", - "Remove key": "Retirer la clé", - "Key description": "Description de la clé", - "Untitled key": "Clé sans titre", - "Click to rename": "Cliquez pour renommer", - "Expired": "Expirée", - "Added": "Ajoutée", - "Addresses": "Adresses", - "No expiry set": "Aucune expiration définie", - "Any address on this account": "Toute adresse de ce compte", - "cryptography\u0004Key": "Clé", - "Public key": "Clé publique", - "Work key": "Clé professionnelle", - "Paste the whole armoured block, headers included. The server checks it and says what is wrong if it cannot read it.": "Collez le bloc ASCII complet, en-têtes compris. Le serveur le vérifie et indique ce qui ne va pas s'il ne peut pas le lire.", - "Add key": "Ajouter la clé", - "Add a key": "Ajouter une clé", - "Stalwart stores these keys, and this release does no more than manage them: ihasmail does not yet sign, encrypt, decrypt or verify anything with them. Adding one does not by itself start encrypting your mail.": "Stalwart conserve ces clés, et cette version ne fait que les gérer : ihasmail ne signe, ne chiffre, ne déchiffre et ne vérifie encore rien avec elles. En ajouter une ne commence pas à chiffrer votre courrier.", - "Unrecognised": "Non reconnue", - "No account to add a key to.": "Aucun compte auquel ajouter une clé.", }, plurals: { // ── Third pass ───────────────────────────────────────────────────── diff --git a/web/src/locales/ja.ts b/web/src/locales/ja.ts index d40645f..1ece5ef 100644 --- a/web/src/locales/ja.ts +++ b/web/src/locales/ja.ts @@ -1312,36 +1312,6 @@ export const catalog: Catalog = { "Empty “{name}”?": "「{name}」を空にしますか?", "Delete them": "削除する", "Nothing was deleted": "何も削除されませんでした", - // ── Encryption keys, over Stalwart's x:PublicKey registry (#67) ── - "Encryption keys": "暗号鍵", - "Not available on this server": "このサーバーでは利用できません", - "Public keys are kept in Stalwart's registry, which this server does not offer. ihasmail needs Stalwart 0.16 or newer for it.": "公開鍵は Stalwart のレジストリに保存されますが、このサーバーはそれを提供していません。ihasmail には Stalwart 0.16 以降が必要です。", - "Public keys for this account — what other people encrypt to, and what a signature is checked against. These are public by nature: no private key is stored, requested, or sent by ihasmail.": "このアカウントの公開鍵です。他の人が暗号化に使い、署名の検証にも使われます。公開鍵は本来公開されるものです。ihasmail は秘密鍵を保存も要求も送信もしません。", - "No keys yet": "鍵はまだありません", - "Add an OpenPGP public key or an S/MIME certificate to publish it on this account.": "OpenPGP 公開鍵または S/MIME 証明書を追加して、このアカウントで公開します。", - "Key added": "鍵を追加しました", - "Key removed": "鍵を削除しました", - "Remove “{name}”?": "「{name}」を削除しますか?", - "this key": "この鍵", - "Anyone holding it can still use it — removing it here only stops this account offering it.": "すでに持っている人は引き続き使用できます。ここで削除しても、このアカウントが提供しなくなるだけです。", - "Remove key": "鍵を削除", - "Key description": "鍵の説明", - "Untitled key": "名前のない鍵", - "Click to rename": "クリックして名前を変更", - "Expired": "期限切れ", - "Added": "追加日", - "Addresses": "アドレス", - "No expiry set": "有効期限なし", - "Any address on this account": "このアカウントのすべてのアドレス", - "cryptography\u0004Key": "鍵", - "Public key": "公開鍵", - "Work key": "仕事用の鍵", - "Paste the whole armoured block, headers included. The server checks it and says what is wrong if it cannot read it.": "ヘッダーを含め、ブロック全体を貼り付けてください。サーバーが検証し、読み取れない場合は理由を表示します。", - "Add key": "鍵を追加", - "Add a key": "鍵を追加", - "Stalwart stores these keys, and this release does no more than manage them: ihasmail does not yet sign, encrypt, decrypt or verify anything with them. Adding one does not by itself start encrypting your mail.": "Stalwart はこれらの鍵を保存し、このリリースでは管理のみを行います。ihasmail はまだ署名も暗号化も復号も検証も行いません。鍵を追加しただけでメールの暗号化が始まるわけではありません。", - "Unrecognised": "認識できません", - "No account to add a key to.": "鍵を追加するアカウントがありません。", }, plurals: { // ── Third pass ───────────────────────────────────────────────────── diff --git a/web/src/locales/nl.ts b/web/src/locales/nl.ts index 031c3f1..05b0789 100644 --- a/web/src/locales/nl.ts +++ b/web/src/locales/nl.ts @@ -1300,36 +1300,6 @@ export const catalog: Catalog = { "Empty “{name}”?": "„{name}” leegmaken?", "Delete them": "Verwijderen", "Nothing was deleted": "Er is niets verwijderd", - // ── Encryption keys, over Stalwart's x:PublicKey registry (#67) ── - "Encryption keys": "Versleutelingssleutels", - "Not available on this server": "Niet beschikbaar op deze server", - "Public keys are kept in Stalwart's registry, which this server does not offer. ihasmail needs Stalwart 0.16 or newer for it.": "Openbare sleutels worden bewaard in het register van Stalwart, dat deze server niet aanbiedt. ihasmail heeft daarvoor Stalwart 0.16 of nieuwer nodig.", - "Public keys for this account — what other people encrypt to, and what a signature is checked against. These are public by nature: no private key is stored, requested, or sent by ihasmail.": "Openbare sleutels voor dit account — waarmee anderen naar u versleutelen, en waaraan een handtekening wordt gecontroleerd. Ze zijn van nature openbaar: ihasmail bewaart, vraagt en verstuurt geen enkele privésleutel.", - "No keys yet": "Nog geen sleutels", - "Add an OpenPGP public key or an S/MIME certificate to publish it on this account.": "Voeg een openbare OpenPGP-sleutel of een S/MIME-certificaat toe om het op dit account te publiceren.", - "Key added": "Sleutel toegevoegd", - "Key removed": "Sleutel verwijderd", - "Remove “{name}”?": "“{name}” verwijderen?", - "this key": "deze sleutel", - "Anyone holding it can still use it — removing it here only stops this account offering it.": "Wie hem al heeft, kan hem blijven gebruiken — hem hier verwijderen zorgt er alleen voor dat dit account hem niet meer aanbiedt.", - "Remove key": "Sleutel verwijderen", - "Key description": "Sleutelomschrijving", - "Untitled key": "Naamloze sleutel", - "Click to rename": "Klik om te hernoemen", - "Expired": "Verlopen", - "Added": "Toegevoegd", - "Addresses": "Adressen", - "No expiry set": "Geen vervaldatum ingesteld", - "Any address on this account": "Elk adres van dit account", - "cryptography\u0004Key": "Sleutel", - "Public key": "Openbare sleutel", - "Work key": "Werksleutel", - "Paste the whole armoured block, headers included. The server checks it and says what is wrong if it cannot read it.": "Plak het hele beveiligde blok, inclusief de kopregels. De server controleert het en zegt wat er mis is als hij het niet kan lezen.", - "Add key": "Sleutel toevoegen", - "Add a key": "Een sleutel toevoegen", - "Stalwart stores these keys, and this release does no more than manage them: ihasmail does not yet sign, encrypt, decrypt or verify anything with them. Adding one does not by itself start encrypting your mail.": "Stalwart bewaart deze sleutels, en deze versie doet niet meer dan ze beheren: ihasmail ondertekent, versleutelt, ontsleutelt en verifieert er nog niets mee. Er een toevoegen begint niet vanzelf uw e-mail te versleutelen.", - "Unrecognised": "Niet herkend", - "No account to add a key to.": "Geen account om een sleutel aan toe te voegen.", }, plurals: { // ── Third pass ───────────────────────────────────────────────────── diff --git a/web/src/locales/pt-BR.ts b/web/src/locales/pt-BR.ts index fb2b6e0..7c11054 100644 --- a/web/src/locales/pt-BR.ts +++ b/web/src/locales/pt-BR.ts @@ -1307,36 +1307,6 @@ export const catalog: Catalog = { "Empty “{name}”?": "Esvaziar “{name}”?", "Delete them": "Excluir todos", "Nothing was deleted": "Nada foi excluído", - // ── Encryption keys, over Stalwart's x:PublicKey registry (#67) ── - "Encryption keys": "Chaves de criptografia", - "Not available on this server": "Não disponível neste servidor", - "Public keys are kept in Stalwart's registry, which this server does not offer. ihasmail needs Stalwart 0.16 or newer for it.": "As chaves públicas ficam no registro do Stalwart, que este servidor não oferece. O ihasmail precisa do Stalwart 0.16 ou mais recente para isso.", - "Public keys for this account — what other people encrypt to, and what a signature is checked against. These are public by nature: no private key is stored, requested, or sent by ihasmail.": "Chaves públicas desta conta — o que outras pessoas usam para criptografar, e aquilo com que uma assinatura é conferida. Elas são públicas por natureza: o ihasmail não armazena, não pede e não envia nenhuma chave privada.", - "No keys yet": "Ainda não há chaves", - "Add an OpenPGP public key or an S/MIME certificate to publish it on this account.": "Adicione uma chave pública OpenPGP ou um certificado S/MIME para publicá-lo nesta conta.", - "Key added": "Chave adicionada", - "Key removed": "Chave removida", - "Remove “{name}”?": "Remover “{name}”?", - "this key": "esta chave", - "Anyone holding it can still use it — removing it here only stops this account offering it.": "Quem já a tem continua podendo usá-la — removê-la aqui apenas faz esta conta parar de oferecê-la.", - "Remove key": "Remover chave", - "Key description": "Descrição da chave", - "Untitled key": "Chave sem título", - "Click to rename": "Clique para renomear", - "Expired": "Expirada", - "Added": "Adicionada", - "Addresses": "Endereços", - "No expiry set": "Sem validade definida", - "Any address on this account": "Qualquer endereço desta conta", - "cryptography\u0004Key": "Chave", - "Public key": "Chave pública", - "Work key": "Chave do trabalho", - "Paste the whole armoured block, headers included. The server checks it and says what is wrong if it cannot read it.": "Cole o bloco blindado inteiro, cabeçalhos incluídos. O servidor confere e diz o que está errado se não conseguir lê-lo.", - "Add key": "Adicionar chave", - "Add a key": "Adicionar uma chave", - "Stalwart stores these keys, and this release does no more than manage them: ihasmail does not yet sign, encrypt, decrypt or verify anything with them. Adding one does not by itself start encrypting your mail.": "O Stalwart guarda essas chaves, e esta versão não faz mais do que gerenciá-las: o ihasmail ainda não assina, criptografa, descriptografa nem verifica nada com elas. Adicionar uma não começa a criptografar seus e-mails por si só.", - "Unrecognised": "Não reconhecida", - "No account to add a key to.": "Não há conta à qual adicionar uma chave.", }, plurals: { // ── Third pass ───────────────────────────────────────────────────── diff --git a/web/src/locales/ru.ts b/web/src/locales/ru.ts index afd7188..0f03961 100644 --- a/web/src/locales/ru.ts +++ b/web/src/locales/ru.ts @@ -1306,36 +1306,6 @@ export const catalog: Catalog = { "Empty “{name}”?": "Очистить «{name}»?", "Delete them": "Удалить их", "Nothing was deleted": "Ничего не удалено", - // ── Encryption keys, over Stalwart's x:PublicKey registry (#67) ── - "Encryption keys": "Ключи шифрования", - "Not available on this server": "Недоступно на этом сервере", - "Public keys are kept in Stalwart's registry, which this server does not offer. ihasmail needs Stalwart 0.16 or newer for it.": "Открытые ключи хранятся в реестре Stalwart, которого этот сервер не предоставляет. Для этого ihasmail требуется Stalwart 0.16 или новее.", - "Public keys for this account — what other people encrypt to, and what a signature is checked against. These are public by nature: no private key is stored, requested, or sent by ihasmail.": "Открытые ключи этой учётной записи — то, чем другие шифруют письма для вас, и то, по чему проверяется подпись. Они открыты по своей природе: ihasmail не хранит, не запрашивает и не отправляет закрытые ключи.", - "No keys yet": "Ключей пока нет", - "Add an OpenPGP public key or an S/MIME certificate to publish it on this account.": "Добавьте открытый ключ OpenPGP или сертификат S/MIME, чтобы опубликовать его в этой учётной записи.", - "Key added": "Ключ добавлен", - "Key removed": "Ключ убран", - "Remove “{name}”?": "Убрать «{name}»?", - "this key": "этот ключ", - "Anyone holding it can still use it — removing it here only stops this account offering it.": "У того, кто его уже получил, он останется — здесь это лишь прекращает предлагать его от этой учётной записи.", - "Remove key": "Убрать ключ", - "Key description": "Описание ключа", - "Untitled key": "Ключ без названия", - "Click to rename": "Нажмите, чтобы переименовать", - "Expired": "Истёк", - "Added": "Добавлен", - "Addresses": "Адреса", - "No expiry set": "Срок действия не задан", - "Any address on this account": "Любой адрес этой учётной записи", - "cryptography\u0004Key": "Ключ", - "Public key": "Открытый ключ", - "Work key": "Рабочий ключ", - "Paste the whole armoured block, headers included. The server checks it and says what is wrong if it cannot read it.": "Вставьте весь блок целиком, вместе с заголовками. Сервер проверит его и сообщит, что не так, если не сможет прочитать.", - "Add key": "Добавить ключ", - "Add a key": "Добавить ключ", - "Stalwart stores these keys, and this release does no more than manage them: ihasmail does not yet sign, encrypt, decrypt or verify anything with them. Adding one does not by itself start encrypting your mail.": "Stalwart хранит эти ключи, а эта версия лишь управляет ими: ihasmail пока ничего ими не подписывает, не шифрует, не расшифровывает и не проверяет. Добавление ключа само по себе не начинает шифровать вашу почту.", - "Unrecognised": "Не распознан", - "No account to add a key to.": "Нет учётной записи, в которую можно добавить ключ.", }, plurals: { // ── Third pass ───────────────────────────────────────────────────── diff --git a/web/src/locales/uk.ts b/web/src/locales/uk.ts index b20b114..860bdad 100644 --- a/web/src/locales/uk.ts +++ b/web/src/locales/uk.ts @@ -1300,36 +1300,6 @@ export const catalog: Catalog = { "Empty “{name}”?": "Очистити «{name}»?", "Delete them": "Видалити їх", "Nothing was deleted": "Нічого не видалено", - // ── Encryption keys, over Stalwart's x:PublicKey registry (#67) ── - "Encryption keys": "Ключі шифрування", - "Not available on this server": "Недоступно на цьому сервері", - "Public keys are kept in Stalwart's registry, which this server does not offer. ihasmail needs Stalwart 0.16 or newer for it.": "Відкриті ключі зберігаються в реєстрі Stalwart, якого цей сервер не надає. Для цього ihasmail потребує Stalwart 0.16 або новішої версії.", - "Public keys for this account — what other people encrypt to, and what a signature is checked against. These are public by nature: no private key is stored, requested, or sent by ihasmail.": "Відкриті ключі цього облікового запису — те, чим інші шифрують листи для вас, і те, за чим перевіряється підпис. Вони відкриті за своєю природою: ihasmail не зберігає, не запитує і не надсилає закритих ключів.", - "No keys yet": "Ключів ще немає", - "Add an OpenPGP public key or an S/MIME certificate to publish it on this account.": "Додайте відкритий ключ OpenPGP або сертифікат S/MIME, щоб опублікувати його в цьому обліковому записі.", - "Key added": "Ключ додано", - "Key removed": "Ключ прибрано", - "Remove “{name}”?": "Прибрати «{name}»?", - "this key": "цей ключ", - "Anyone holding it can still use it — removing it here only stops this account offering it.": "У того, хто вже його має, він залишиться — тут це лише припиняє пропонувати його від цього облікового запису.", - "Remove key": "Прибрати ключ", - "Key description": "Опис ключа", - "Untitled key": "Ключ без назви", - "Click to rename": "Натисніть, щоб перейменувати", - "Expired": "Закінчився", - "Added": "Додано", - "Addresses": "Адреси", - "No expiry set": "Термін дії не задано", - "Any address on this account": "Будь-яка адреса цього облікового запису", - "cryptography\u0004Key": "Ключ", - "Public key": "Відкритий ключ", - "Work key": "Робочий ключ", - "Paste the whole armoured block, headers included. The server checks it and says what is wrong if it cannot read it.": "Вставте весь блок цілком, разом із заголовками. Сервер перевірить його і скаже, що не так, якщо не зможе прочитати.", - "Add key": "Додати ключ", - "Add a key": "Додати ключ", - "Stalwart stores these keys, and this release does no more than manage them: ihasmail does not yet sign, encrypt, decrypt or verify anything with them. Adding one does not by itself start encrypting your mail.": "Stalwart зберігає ці ключі, а ця версія лише керує ними: ihasmail поки нічого ними не підписує, не шифрує, не розшифровує і не перевіряє. Додавання ключа саме собою не починає шифрувати вашу пошту.", - "Unrecognised": "Не розпізнано", - "No account to add a key to.": "Немає облікового запису, до якого можна додати ключ.", }, plurals: { // ── Third pass ───────────────────────────────────────────────────── diff --git a/web/src/locales/zh-Hans.ts b/web/src/locales/zh-Hans.ts index 728db6f..869eff7 100644 --- a/web/src/locales/zh-Hans.ts +++ b/web/src/locales/zh-Hans.ts @@ -1311,36 +1311,6 @@ export const catalog: Catalog = { "Empty “{name}”?": "清空“{name}”?", "Delete them": "删除", "Nothing was deleted": "未删除任何内容", - // ── Encryption keys, over Stalwart's x:PublicKey registry (#67) ── - "Encryption keys": "加密密钥", - "Not available on this server": "此服务器不支持", - "Public keys are kept in Stalwart's registry, which this server does not offer. ihasmail needs Stalwart 0.16 or newer for it.": "公钥保存在 Stalwart 的注册表中,而此服务器未提供该功能。ihasmail 需要 Stalwart 0.16 或更高版本。", - "Public keys for this account — what other people encrypt to, and what a signature is checked against. These are public by nature: no private key is stored, requested, or sent by ihasmail.": "此账户的公钥——他人用它加密,签名也据此校验。公钥本就是公开的:ihasmail 不会存储、索取或发送任何私钥。", - "No keys yet": "尚无密钥", - "Add an OpenPGP public key or an S/MIME certificate to publish it on this account.": "添加 OpenPGP 公钥或 S/MIME 证书,将其发布到此账户。", - "Key added": "已添加密钥", - "Key removed": "已移除密钥", - "Remove “{name}”?": "移除「{name}」?", - "this key": "此密钥", - "Anyone holding it can still use it — removing it here only stops this account offering it.": "已经持有它的人仍可继续使用——在此移除只会让此账户不再提供它。", - "Remove key": "移除密钥", - "Key description": "密钥说明", - "Untitled key": "未命名密钥", - "Click to rename": "点击以重命名", - "Expired": "已过期", - "Added": "添加时间", - "Addresses": "地址", - "No expiry set": "未设置有效期", - "Any address on this account": "此账户的任意地址", - "cryptography\u0004Key": "密钥", - "Public key": "公钥", - "Work key": "工作密钥", - "Paste the whole armoured block, headers included. The server checks it and says what is wrong if it cannot read it.": "请粘贴完整的密钥文本块,包括头尾行。服务器会进行校验,无法读取时会说明原因。", - "Add key": "添加密钥", - "Add a key": "添加密钥", - "Stalwart stores these keys, and this release does no more than manage them: ihasmail does not yet sign, encrypt, decrypt or verify anything with them. Adding one does not by itself start encrypting your mail.": "Stalwart 会保存这些密钥,而此版本仅负责管理:ihasmail 尚不会用它们签名、加密、解密或验证。添加密钥本身并不会开始加密您的邮件。", - "Unrecognised": "无法识别", - "No account to add a key to.": "没有可添加密钥的账户。", }, plurals: { // ── Third pass ───────────────────────────────────────────────────── diff --git a/web/src/views/settings/KeysSettings.tsx b/web/src/views/settings/KeysSettings.tsx deleted file mode 100644 index 9afad3f..0000000 --- a/web/src/views/settings/KeysSettings.tsx +++ /dev/null @@ -1,212 +0,0 @@ -import { useCallback, useEffect, useState } from "react"; -import { KeyRound, Plus, Trash2 } from "lucide-react"; -import { - addPublicKey, - isExpired, - keyExcerpt, - keyKind, - keyKindLabel, - listPublicKeys, - publicKeysAvailable, - removePublicKey, - updatePublicKey, - type PublicKey, -} from "@/lib/publicKeys"; -import { formatFullDate } from "@/lib/format"; -import { confirmDialog } from "@/ui/dialog"; -import { Empty, Spinner } from "@/ui/misc"; -import { toast } from "@/ui/toast"; -import { t, tc } from "@/lib/i18n"; - -/** - * Public keys for this account: the ones other people encrypt to, and the ones - * a signature is checked against. Nothing here handles a private key, and - * nothing here asks for one. - * - * The page is careful not to overstate what adding a key achieves. Stalwart - * stores keys, and `encryptionAtRest` on `x:AccountSettings` is the one thing - * known to consume one; nothing in ihasmail signs, encrypts, decrypts or - * verifies with them yet. Saying otherwise would be a guess dressed as a - * feature. - */ -export function KeysSettings() { - const [keys, setKeys] = useState(null); - const [error, setError] = useState(null); - const [adding, setAdding] = useState(false); - const [busy, setBusy] = useState(false); - const [editing, setEditing] = useState(null); - const [draftKey, setDraftKey] = useState(""); - const [draftName, setDraftName] = useState(""); - const available = publicKeysAvailable(); - - const load = useCallback(async () => { - if (!available) { - setKeys([]); - return; - } - try { - setKeys(await listPublicKeys()); - setError(null); - } catch (err) { - setError((err as Error).message); - setKeys([]); - } - }, [available]); - - useEffect(() => { - void load(); - }, [load]); - - const add = async () => { - if (!draftKey.trim()) return; - setBusy(true); - try { - // An empty description is left empty rather than filled in with an - // English default: the description is stored on the server, so a name - // invented here would be whatever language the adder happened to use. - // The list labels a blank one at render time instead. - await addPublicKey(draftKey, draftName); - setDraftKey(""); - setDraftName(""); - setAdding(false); - toast.success(t("Key added")); - await load(); - } catch (err) { - // Stalwart parsed the key and knows exactly what is wrong with it, in - // more detail than anything invented here could manage. - toast.error((err as Error).message); - } finally { - setBusy(false); - } - }; - - const rename = async (k: PublicKey, description: string) => { - if (description === k.description) return; - try { - await updatePublicKey(k.id, { description }); - await load(); - } catch (err) { - toast.error((err as Error).message); - await load(); - } - }; - - const remove = async (k: PublicKey) => { - const ok = await confirmDialog({ - title: t("Remove “{name}”?", { name: k.description || t("this key") }), - message: t("Anyone holding it can still use it — removing it here only stops this account offering it."), - confirmLabel: t("Remove key"), - danger: true, - }); - if (!ok) return; - try { - await removePublicKey(k.id); - toast.success(t("Key removed")); - } catch (err) { - toast.error((err as Error).message); - } - await load(); - }; - - if (!available) { - return ( -
-

{t("Encryption keys")}

- } title={t("Not available on this server")}> - {t("Public keys are kept in Stalwart's registry, which this server does not offer. ihasmail needs Stalwart 0.16 or newer for it.")} - -
- ); - } - - return ( -
-

{t("Encryption keys")}

-

- {t("Public keys for this account — what other people encrypt to, and what a signature is checked against. These are public by nature: no private key is stored, requested, or sent by ihasmail.")} -

- - {error &&
{error}
} - - {keys === null ? ( -
- ) : keys.length === 0 && !adding ? ( - } title={t("No keys yet")}> - {t("Add an OpenPGP public key or an S/MIME certificate to publish it on this account.")} - - ) : ( - keys.map((k) => { - const kind = keyKind(k.key); - const expired = isExpired(k); - return ( -
-
- - {editing === k.id ? ( - { void rename(k, e.target.value.trim() || k.description); setEditing(null); }} - onKeyDown={(e) => { if (e.key === "Enter") (e.target as HTMLInputElement).blur(); if (e.key === "Escape") setEditing(null); }} - /> - ) : ( -

setEditing(k.id)} title={t("Click to rename")}>{k.description || t("Untitled key")}

- )} - {keyKindLabel(kind)} - {expired && {t("Expired")}} - -
- - - - - - - -
{t("Added")}{k.createdAt ? formatFullDate(k.createdAt) : "—"}
{t("Expires")}{k.expiresAt ? formatFullDate(k.expiresAt) : t("No expiry set")}
{t("Addresses")}{k.emailAddresses.length ? k.emailAddresses.join(", ") : t("Any address on this account")}
{tc("cryptography", "Key")}{keyExcerpt(k.key)}…
-
- ); - }) - )} - - {adding ? ( -
-
- - setDraftName(e.target.value)} /> -
-
- -