The hint under the theme picker listed the third-party palettes by name.
That sentence is translated into nine languages, so every palette added
meant rewriting it, retranslating it nine times, and leaving the previous
version behind as a stale key nothing looks up.
It now describes the rule instead of enumerating the cases: a palette
named after another project is that project's work, used under its own
licence. True of the four here, true of the next one, and true without
saying "MIT" for a palette that might not be. The names are already in
Settings beside each swatch and in NOTICE with their copyright lines,
which is where a credit belongs.
Swapped rather than added in all nine catalogues, so the old key is gone
rather than left stale: 1,255 of 1,279 translated per locale, unchanged,
and the 41 pre-existing stale keys are neither added to nor cleaned up
here.
Appearance gained "Apply the theme to messages too" some time ago, and it
themes an HTML message only when the message brings no colours of its own.
That predicate is the right default and it almost never passes: one
`color:#FFFFFF` on one button label opts a whole message out, so in real
mail — receipts, shipping notices, anything from a template — the switch
did nothing at all and the reader kept a bright white card on a dark UI.
A second switch, off by default and only meaningful with the first on,
forces the palette over the sender's colours. It cannot be done perfectly,
which is why it is a separate, explicit choice: the same bargain a
dark-reader extension makes.
What it does is tell two kinds of colour apart. A *sheet* the design sits
on — the white 600px wrapper — is neutralised, and a *painted surface* —
a call to action, a footer banner — is kept whole so its label stays
legible on it. Relative luminance decides, at 0.5: white wrappers sit at
1.0, a blue button near 0.09. Only the painted ones are marked, with
data-ihm-keep, and one rule in EMAIL_BASE_CSS neutralises everything else.
Nothing the sender wrote is removed, so the switch is reversible, colours
arriving from a <style> block are covered as well as inline ones, and
print still pins the tokens to ink on white.
The mock grew the message this is about: an outer wrapper on
bgcolor="#ffffff", a <style> block, a coloured button, a grey footer.
Without one, neither the bug nor the fix could be seen.
Verified in a browser against the mock: with only the first switch on the
card is still white; with both, the wrapper computes to transparent, body
text follows the theme, and the button keeps white-on-blue. Two surfaces
marked, which are the two the message paints.
Closes#290
A signed message now says whether that holds up, as it is read. This is
verification only: nothing here signs, encrypts or decrypts, and the
private-key question that blocks those is untouched. Verifying needed
none of it, because the certificate travels inside the message -- which
is why this is the half that could be built.
What it checks. For multipart/signed carrying PKCS#7, the exact bytes of
the signed part -- headers included, canonicalised to CRLF -- are hashed
against the messageDigest attribute, and the signature over the signed
attributes is verified with WebCrypto against the certificate inside the
message. RSA PKCS#1 v1.5 and ECDSA over P-256/384/521, with SHA-256, 384
or 512.
The trust model is the design, and it is deliberately small. A browser
has no system trust store, and the certificate arrives inside the
message, so anyone can self-sign as anyone: on its own a good signature
shows only that the sender held the key they attached. So the word
"verified" is never rendered, and the reassuring case is not the loud
one. What carries the weight is remembering -- the first signed message
from an address pins its fingerprint, later ones are compared, and a
signer that changed is reported with both names and told to check by
another route. Trust on first use, no certificate authority anywhere.
The pins live in the account's settings rather than the browser: one
that only a single device knew would greet the same correspondent as new
everywhere else, which is how people are trained to click past the one
warning that matters. A pin records the message that created it, so the
message that established a signer keeps saying so instead of appearing
to be corroborated by itself -- without that, the very first signed
message anybody receives reads as "the same signer as before", where
before is itself. A changed, mismatched or expired signer is never
pinned, since writing the anomaly into the baseline makes every later
message agree with it.
Three things are declined rather than attempted, and all three say
"could not check" rather than "does not check out", because ignorance
and an accusation are different claims:
- OpenPGP, by name. The signature carries no key and there is nowhere
to get the sender's: x:PublicKey is the account's OWN registry, and
a keyserver or WKD lookup would tell a third party who you
correspond with -- the leak the image proxy exists to close.
- SHA-1. Not forgeable in practice today, still not something to put a
tick beside.
- RSA-PSS, whose salt length lives in parameters this does not read.
Guessing wrong would report a good signature as bad.
Nothing validates a chain: no CA bundle is shipped and revocation is not
checked. "Issued by" reports what the certificate claims, and a
self-signed one claims itself.
The DER, CMS, X.509 and MIME readers are hand-written and deliberately
narrow -- no new dependency, and the whole verifier is a lazily imported
8.6 kB chunk that a reader of unsigned mail never downloads. The one
place this is easy to get quietly wrong has its own function and its own
test: signed attributes are signed as a SET OF, not as the [0] IMPLICIT
they arrive as, and hashing the message instead would make every
signature "pass".
Tested against real `openssl smime -sign` output rather than hand-built
fixtures -- RSA, ECDSA, a tampered copy, and a valid signature by a
certificate for somebody else -- because a signed message written by
hand only agrees with whatever its author believed the format to be.
Also driven in a browser against the mock, which now serves three real
signed messages so every branch of the banner is reachable.
Translations: 34 new strings in all nine catalogues, 306 entries.
Falling back to English is unchanged at 24 per language.
Raised on #174 as the other half of a migration -- import, notice
something is wrong, empty the book, correct the export, import again --
and tracked as #277.
The gap turned out to be wider than the ask. Contacts had no multi-select
at all: the only delete in the module was the cross on a single card's
pane, one card and one confirmation at a time. `destroyCards` has taken a
list and batched it against maxObjectsInSet since #218, and nothing in
the UI ever handed it more than one id. So "empty this address book" was
missing, and so was "delete these fourteen".
The list now has checkboxes, on hover the way the message list's are, and
always on a touchscreen where there is no hover to reveal them.
Shift-click takes the run between two rows. The search box gives way to a
selection bar rather than sitting beside it, because what the count
promises is what the search left on screen. A selection is cleared when
the book being shown changes, since carrying it across would leave a
count describing rows that are no longer there and a Delete aimed at
them.
Emptying a book is in the book's own menu, beside the import and export
that moved there in #226, and separate from Delete, which takes the book
with it. A default book cannot be deleted and can perfectly well be
emptied, which is most of the reason it is its own entry.
The part that is not a deletion, and the reason this is not one destroy
over everything in the book: a card filed in two books belongs to both,
and `ContactCard/set destroy` takes it away from both at once. Emptying
one book must not empty another, so a card with a second home is patched
out of this one and left alone. That is reported separately afterwards,
because it would otherwise look like contacts that refused to go.
`destroyCards` now answers with what the server confirmed rather than
throwing on the first refusal. A refusal that took half a selection with
it still deleted the other half, and an error saying only that it failed
sends somebody looking for contacts that are already gone. Both callers
report the count and the reason apart.
Emptying a shared book is deliberately not offered: the cards live in the
owner's account and this client has no path to write there.
One bug found by driving the built app rather than by any test, and worth
recording because of where it hid. The range a shift-click covers was
measured inside the `setPicked` updater -- which React runs when it gets
round to rendering, by which time the anchor ref has already been moved
to the row that *ended* the range. Every shift-click selected exactly one
row, and every store assertion still passed, because nothing was wrong
below the component. The anchor is read before the updater now, and the
contacts view has its first component tests: ten of them, six of which
fail if the measurement moves back inside.
Twelve new strings, in all nine catalogues, so nothing new falls back to
English.
Found by widening the coverage check to plural() forms in every file rather
than the two being worked on. Seven counted strings in the Files view and the
event editor had never been in any of the nine catalogues, so they rendered in
English whatever language was chosen.
Not a regression from the recent work -- they have been missing since the
features landed, and every earlier scan looked at t("literal") sites and the
plurals of whichever file was in hand.
All nine languages, one commit rather than nine: this is a single gap in a
check rather than a translation pass, and splitting it per language would
suggest nine decisions where there is one.
#261 rebuilt the Sieve rule summaries and the recurrence descriptions as whole
sentences with placeholders, so that a translator can move the parts rather
than being handed " and " on its own. This is the Ukrainian half of that.
32 strings and 9 plural forms. The ordinals are words -- there is no suffix to
append here, which was the point -- and the day and item lists are joined by
Intl.ListFormat rather than a translated separator.
#261 rebuilt the Sieve rule summaries and the recurrence descriptions as whole
sentences with placeholders, so that a translator can move the parts rather
than being handed " and " on its own. This is the Russian half of that.
32 strings and 9 plural forms. The ordinals are words -- there is no suffix to
append here, which was the point -- and the day and item lists are joined by
Intl.ListFormat rather than a translated separator.
#261 rebuilt the Sieve rule summaries and the recurrence descriptions as whole
sentences with placeholders, so that a translator can move the parts rather
than being handed " and " on its own. This is the Simplified Chinese half of that.
32 strings and 9 plural forms. The ordinals are words -- there is no suffix to
append here, which was the point -- and the day and item lists are joined by
Intl.ListFormat rather than a translated separator.
#261 rebuilt the Sieve rule summaries and the recurrence descriptions as whole
sentences with placeholders, so that a translator can move the parts rather
than being handed " and " on its own. This is the Japanese half of that.
32 strings and 9 plural forms. The ordinals are words -- there is no suffix to
append here, which was the point -- and the day and item lists are joined by
Intl.ListFormat rather than a translated separator.
#261 rebuilt the Sieve rule summaries and the recurrence descriptions as whole
sentences with placeholders, so that a translator can move the parts rather
than being handed " and " on its own. This is the Brazilian Portuguese half of that.
32 strings and 9 plural forms. The ordinals are words -- there is no suffix to
append here, which was the point -- and the day and item lists are joined by
Intl.ListFormat rather than a translated separator.
#261 rebuilt the Sieve rule summaries and the recurrence descriptions as whole
sentences with placeholders, so that a translator can move the parts rather
than being handed " and " on its own. This is the Dutch half of that.
32 strings and 9 plural forms. The ordinals are words -- there is no suffix to
append here, which was the point -- and the day and item lists are joined by
Intl.ListFormat rather than a translated separator.
#261 rebuilt the Sieve rule summaries and the recurrence descriptions as whole
sentences with placeholders, so that a translator can move the parts rather
than being handed " and " on its own. This is the French half of that.
32 strings and 9 plural forms. The ordinals are words -- there is no suffix to
append here, which was the point -- and the day and item lists are joined by
Intl.ListFormat rather than a translated separator.
#261 rebuilt the Sieve rule summaries and the recurrence descriptions as whole
sentences with placeholders, so that a translator can move the parts rather
than being handed " and " on its own. This is the Spanish half of that.
32 strings and 9 plural forms. The ordinals are words -- there is no suffix to
append here, which was the point -- and the day and item lists are joined by
Intl.ListFormat rather than a translated separator.
#261 rebuilt the Sieve rule summaries and the recurrence descriptions as whole
sentences with placeholders, so that a translator can move the parts rather
than being handed " and " on its own. This is the German half of that.
32 strings and 9 plural forms. The ordinals are words -- there is no suffix to
append here, which was the point -- and the day and item lists are joined by
Intl.ListFormat rather than a translated separator.
The sweep in #259 found user-facing English in lib/ and store/ -- scheduled
send, the read-receipt explanations, compose toasts and thrown errors -- plus
two swipe labels that reach t() through a variable and so were invisible to a
scan for t("literal") call sites.
17 strings and 3 plural forms. "{n} days" and "{n} hours" replace a suffix
appended in the code, which is English grammar the catalogue could not reach.
The sweep in #259 found user-facing English in lib/ and store/ -- scheduled
send, the read-receipt explanations, compose toasts and thrown errors -- plus
two swipe labels that reach t() through a variable and so were invisible to a
scan for t("literal") call sites.
17 strings and 3 plural forms. "{n} days" and "{n} hours" replace a suffix
appended in the code, which is English grammar the catalogue could not reach.
The sweep in #259 found user-facing English in lib/ and store/ -- scheduled
send, the read-receipt explanations, compose toasts and thrown errors -- plus
two swipe labels that reach t() through a variable and so were invisible to a
scan for t("literal") call sites.
17 strings and 3 plural forms. "{n} days" and "{n} hours" replace a suffix
appended in the code, which is English grammar the catalogue could not reach.
The sweep in #259 found user-facing English in lib/ and store/ -- scheduled
send, the read-receipt explanations, compose toasts and thrown errors -- plus
two swipe labels that reach t() through a variable and so were invisible to a
scan for t("literal") call sites.
17 strings and 3 plural forms. "{n} days" and "{n} hours" replace a suffix
appended in the code, which is English grammar the catalogue could not reach.
The sweep in #259 found user-facing English in lib/ and store/ -- scheduled
send, the read-receipt explanations, compose toasts and thrown errors -- plus
two swipe labels that reach t() through a variable and so were invisible to a
scan for t("literal") call sites.
17 strings and 3 plural forms. "{n} days" and "{n} hours" replace a suffix
appended in the code, which is English grammar the catalogue could not reach.
The sweep in #259 found user-facing English in lib/ and store/ -- scheduled
send, the read-receipt explanations, compose toasts and thrown errors -- plus
two swipe labels that reach t() through a variable and so were invisible to a
scan for t("literal") call sites.
17 strings and 3 plural forms. "{n} days" and "{n} hours" replace a suffix
appended in the code, which is English grammar the catalogue could not reach.
The sweep in #259 found user-facing English in lib/ and store/ -- scheduled
send, the read-receipt explanations, compose toasts and thrown errors -- plus
two swipe labels that reach t() through a variable and so were invisible to a
scan for t("literal") call sites.
17 strings and 3 plural forms. "{n} days" and "{n} hours" replace a suffix
appended in the code, which is English grammar the catalogue could not reach.
The sweep in #259 found user-facing English in lib/ and store/ -- scheduled
send, the read-receipt explanations, compose toasts and thrown errors -- plus
two swipe labels that reach t() through a variable and so were invisible to a
scan for t("literal") call sites.
17 strings and 3 plural forms. "{n} days" and "{n} hours" replace a suffix
appended in the code, which is English grammar the catalogue could not reach.
The sweep in #259 found user-facing English in lib/ and store/ -- scheduled
send, the read-receipt explanations, compose toasts and thrown errors -- plus
two swipe labels that reach t() through a variable and so were invisible to a
scan for t("literal") call sites.
17 strings and 3 plural forms. "{n} days" and "{n} hours" replace a suffix
appended in the code, which is English grammar the catalogue could not reach.
Features landed after the catalogues were written and their strings were never
added, so they rendered in English. Reported against German (#247); every
language had the identical gap.
176 entries: 153 from features that shipped after the translation pass, and 23
keyboard bindings whose group and description are registered in English at the
call site and translated at render.
Follows the decisions this file already pins: 您 where the reader is addressed
directly and the pronoun dropped everywhere it can be, and the fixed
terminology, so 收件箱, 文件夹, 邮件, 会话, 标签, 已删除邮件, 草稿 and 设置
read the same here as everywhere above.
Fifteen strings are deliberately absent and keep falling back to English:
example.com and the other input placeholders, the product names, and the
verbatim header names List-Id and X-Spam-Status.
Verified: typecheck clean, 1000 tests pass, nothing missing from zh-Hans.ts.
Features landed after the catalogues were written and their strings were never
added, so they rendered in English. Reported against German (#247); every
language had the identical gap.
176 entries: 153 from features that shipped after the translation pass, and 23
keyboard bindings whose group and description are registered in English at the
call site and translated at render.
Follows the decisions this file already pins: ви in lowercase, the infinitive
for actions, and the terminology that keeps this a Ukrainian catalogue rather
than the Russian one with a different name on it -- Тека and not папка, Мітка
and not ярлик, which in Ukrainian means a shortcut. Вхідні, Лист, Листування,
Кошик, Чернетки and Налаштування read the same here as everywhere above.
Fifteen strings are deliberately absent and keep falling back to English:
example.com and the other input placeholders, the product names, and the
verbatim header names List-Id and X-Spam-Status.
Verified: typecheck clean, 1000 tests pass, nothing missing from uk.ts.
Features landed after the catalogues were written and their strings were never
added, so they rendered in English. Reported against German (#247); every
language had the identical gap.
176 entries: 153 from features that shipped after the translation pass, and 23
keyboard bindings whose group and description are registered in English at the
call site and translated at render.
Follows the decisions this file already pins: вы in lowercase rather than
correspondence-style «Вы», the infinitive for actions, and the fixed
terminology, so Входящие, Папка, Письмо, Цепочка, Ярлык, Корзина, Черновики
and Настройки read the same here as everywhere above.
Fifteen strings are deliberately absent and keep falling back to English:
example.com and the other input placeholders, the product names, and the
verbatim header names List-Id and X-Spam-Status.
Verified: typecheck clean, 1000 tests pass, nothing missing from ru.ts.
Features landed after the catalogues were written and their strings were never
added, so they rendered in English. Reported against German (#247); every
language had the identical gap.
176 entries: 153 from features that shipped after the translation pass, and 23
keyboard bindings whose group and description are registered in English at the
call site and translated at render.
Follows the decisions this file already pins: です・ます throughout, no あなた,
bare noun or verb stem on buttons, and the deliberate script mixing -- kanji
for the noun carrying the meaning, katakana for the loanword the reader knows,
long vowels keeping their ー. So 受信トレイ, フォルダー, メール, スレッド,
ラベル, ゴミ箱, 下書き and 設定 read the same here as everywhere above.
Fifteen strings are deliberately absent and keep falling back to English:
example.com and the other input placeholders, the product names, and the
verbatim header names List-Id and X-Spam-Status.
Verified: typecheck clean, 1000 tests pass, nothing missing from ja.ts.
Features landed after the catalogues were written and their strings were never
added, so they rendered in English. Reported against German (#247); every
language had the identical gap.
176 entries: 153 from features that shipped after the translation pass, and 23
keyboard bindings whose group and description are registered in English at the
call site and translated at render.
Follows the decisions this file already pins: você rather than the formal
address the other Phase 1 languages took, and the fixed terminology, so Caixa
de entrada, Pasta, Mensagem, Conversa, Marcador, Lixeira, Rascunhos and
Configurações read the same here as everywhere above.
Fifteen strings are deliberately absent and keep falling back to English:
example.com and the other input placeholders, the product names, and the
verbatim header names List-Id and X-Spam-Status.
Verified: typecheck clean, 1000 tests pass, nothing missing from pt-BR.ts.
Features landed after the catalogues were written and their strings were never
added, so they rendered in English. Reported against German (#247); every
language had the identical gap.
176 entries: 153 from features that shipped after the translation pass, and 23
keyboard bindings whose group and description are registered in English at the
call site and translated at render.
Follows the decisions this file already pins: u throughout, and the fixed
terminology, so Postvak IN, Map, Bericht, Gesprek, Label, Prullenbak,
Concepten and Instellingen read the same here as everywhere above.
Fifteen strings are deliberately absent and keep falling back to English:
example.com and the other input placeholders, the product names, and the
verbatim header names List-Id and X-Spam-Status.
Verified: typecheck clean, 1000 tests pass, nothing missing from nl.ts.
Features landed after the catalogues were written and their strings were never
added, so they rendered in English. Reported against German (#247); every
language had the identical gap.
176 entries: 153 from features that shipped after the translation pass, and 23
keyboard bindings whose group and description are registered in English at the
call site and translated at render.
Follows the decisions this file already pins: vous throughout, guillemets for
quoted names, a plain space before ? and : rather than a narrow no-break one,
and the fixed terminology, so Boîte de réception, Dossier, Message, Libellé,
Corbeille, Brouillons and Paramètres read the same here as everywhere above.
Fifteen strings are deliberately absent and keep falling back to English:
example.com and the other input placeholders, the product names, and the
verbatim header names List-Id and X-Spam-Status.
Verified: typecheck clean, 1000 tests pass, nothing missing from fr.ts.
Features landed after the catalogues were written and their strings were never
added, so they rendered in English. Reported against German (#247); every
language had the identical gap.
176 entries: 153 from features that shipped after the translation pass, and 23
keyboard bindings whose group and description are registered in English at the
call site and translated at render.
Follows the decisions this file already pins: usted throughout, peninsular
Spanish, and the fixed terminology, so Bandeja de entrada, Carpeta, Mensaje,
Conversación, Etiqueta, Papelera, Borradores and Configuración read the same
here as everywhere above.
Fifteen strings are deliberately absent and keep falling back to English:
example.com and the other input placeholders, the product names, and the
verbatim header names List-Id and X-Spam-Status.
Verified: typecheck clean, 1000 tests pass, nothing missing from es.ts.
Features landed after the catalogues were written and their strings were never
added, so they rendered in English. A native speaker reviewing this file
reported it (#247), which is the review the language was marked Beta waiting
for.
176 entries: 153 from features that shipped after the translation pass --
privacy and safety settings, the message ordering controls, subscribed and
birthday calendars, label nesting, the file preview, the unsaved-changes
prompts -- and 23 keyboard bindings, whose group and description are
registered in English at the call site and are translated at render.
Follows the decisions this file already pins: Sie throughout, and the fixed
terminology, so Ordner, Nachricht, Konversation, Posteingang, Papierkorb,
Entwürfe and Label read the same here as everywhere above.
Fifteen strings are deliberately absent and keep falling back to English,
which is the correct rendering for them: example.com and the other input
placeholders, the product names, and the verbatim header names List-Id and
X-Spam-Status. A translator should not be invited to translate example.com.
Verified: typecheck clean, 1000 tests pass, and a coverage pass over every
t() call site reports nothing missing from de.ts.
#228 skipped a vCard whose UID the book already held. The reporter asked for
the opposite on #174 and he is right: the reason to import a file a second
time is usually that the first one was not right, so skipping means a
corrected export corrects nothing.
A merge, not a replacement. Properties the file carries overwrite what is
here; properties it does not mention are left alone, so a phone number added
in ihasmail after the first import survives a re-import of the original file.
The cost is that a field genuinely deleted at the source stays here, which is
the better way to be wrong -- the other way round loses work nobody asked to
lose. Worth confirming with him rather than assuming.
`addressBookIds` is left off the patch. The card is already in this book, so
saying it again says nothing, and saying it on a card that is also in another
book would move it.
Creates and updates now share one batch budget. Stalwart counts every object
in a /set together, so batching the halves separately would send 300 new and
300 changed as two calls of 300 and be refused for a limit of 500 that neither
half exceeds.
LDIF is untouched and still reports look-alikes without acting on them, since
what it should match on is the question still open on #223. Both imports keep
one answer shape so a caller need not know which it called; LDIF's `updated`
is always 0, which is the honest number rather than a missing field.
The message a vCard attached to a message shows changes with it: the newer
copy now wins instead of being dropped, so it says the contact was brought up
to date rather than that nothing was added.
Refs #223.
The hint at the foot of Message headers named an action and left you to go
find it. Requested in #236, and the reporter is right that it is the shape of
the thing rather than the size: telling somebody a feature exists is half a
job when the other half is one element away.
Clicking it now closes the headers dialog and opens the original, so it reads
as going deeper rather than as opening a second window.
`tNode` rather than a sentence chopped either side of a button: the sentence
stays whole for whoever translates it, and a language that puts the verb
somewhere else can move the hole rather than being handed two fragments.
The link style needed unscoping to work, which turned out to be a bug of its
own. `.link-btn` was written for the composer's To and Cc labels and scoped to
`.composer-field label`, so the two callers outside it -- the trusted-domain
list in Privacy settings, and now this -- rendered as default button chrome in
the middle of a sentence. The rule is now unscoped and Privacy is fixed by the
same change.
Checked in a browser: the hint reads as a sentence with a dotted-underlined
link in it, clicking swaps one dialog for the other, and the raw message is
there.
Closes#236.