diff --git a/Dockerfile b/Dockerfile
index cc6858f..f0d0d7d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -34,6 +34,18 @@ RUN set -eux; \
done; \
python3 /sprak.py --sjekk /locales/nb/messages.json
+# E-postmalene bygges av maler.py, ett sett per språk. Serveren peker på ett av
+# settene per kunde, så en engelsktalende bedrift kan få engelske e-poster uten
+# at noen tekst skrives to steder.
+FROM alpine:3.21@sha256:48b0309ca019d89d40f670aa1bc06e426dc0931948452e8491e3d65087abc07d AS maler
+RUN apk add --no-cache python3
+COPY --from=kilde /vaultwarden /vaultwarden
+ARG MAIL_DIR=vault-image/mail
+COPY ${MAIL_DIR}/maler.py /maler.py
+# Første kall sammenligner med malene serveren faktisk kan sende: kommer det en
+# ny i en oppgradering, stopper bygget her i stedet for at den går ut på engelsk.
+RUN python3 /maler.py --mot /vaultwarden && python3 /maler.py /maler
+
FROM vaultwarden/server:${VW_VERSION}@${VW_DIGEST}
COPY --from=sprak /locales /web-vault/locales
@@ -44,8 +56,7 @@ COPY ${BRAND_DIR}/ /web-vault/_vaultnord/
# Egne e-postmaler. Uten disse går invitasjonene ut med Vaultwardens navn og
# utseende - det er kundens første inntrykk av tjenesten, og det skal være vårt.
-ARG MAIL_DIR=vault-image/mail
-COPY ${MAIL_DIR}/ /vaultnord/templates/
+COPY --from=maler /maler/ /vaultnord/templates/
RUN set -eux; \
cp /web-vault/_vaultnord/favicon.png /web-vault/images/favicon-32x32.png; \
diff --git a/mail/email/invite_accepted.hbs b/mail/email/invite_accepted.hbs
deleted file mode 100644
index a3c8be9..0000000
--- a/mail/email/invite_accepted.hbs
+++ /dev/null
@@ -1,12 +0,0 @@
-{{{email}}} har takket ja til invitasjonen
-
-{{email}} har satt hovedpassordet sitt og takket ja til invitasjonen til {{org_name}}.
-
-Siste steg må gjøres av deg: logg inn på {{url}} og bekreft brukeren under
-Medlemmer i organisasjonen. Først da får de tilgang til det som deles.
-
-Bekreftelsen kan bare gjøres av deg - den krever organisasjonsnøkkelen, som
-bare finnes i din egen innloggede klient. Det er derfor vi ikke kan gjøre det
-for deg.
-
-Vaultnord
diff --git a/mail/email/invite_accepted.html.hbs b/mail/email/invite_accepted.html.hbs
deleted file mode 100644
index e4cdc19..0000000
--- a/mail/email/invite_accepted.html.hbs
+++ /dev/null
@@ -1,21 +0,0 @@
-{{{email}}} har takket ja til invitasjonen
-
-
-
-
-
-
- | vaultnord |
-
-{{email}} har takket ja
- {{email}} har satt hovedpassordet sitt og takket ja til invitasjonen til {{org_name}}.
- Siste steg må gjøres av deg: logg inn på hvelvet og bekreft brukeren under Medlemmer. Først da får de tilgang til det som deles.
- Bekreftelsen krever organisasjonsnøkkelen, som bare finnes i din egen innloggede klient — derfor kan ikke vi gjøre den for deg.
- |
- |
- Vaultnord · drift i Finland, backup i Tyskland — alt innenfor EU.
- |
-
- |
-
-
diff --git a/mail/email/invite_confirmed.hbs b/mail/email/invite_confirmed.hbs
deleted file mode 100644
index 15d43e8..0000000
--- a/mail/email/invite_confirmed.hbs
+++ /dev/null
@@ -1,10 +0,0 @@
-Du har fått tilgang til {{{org_name}}}
-
-Du er nå bekreftet som medlem av {{org_name}}.
-
-Passordene organisasjonen deler med deg dukker opp i hvelvet ditt: {{url}}
-
-Bruker du Bitwarden-appen eller nettleserutvidelsen, kan du måtte synkronisere
-en gang for å se dem.
-
-Vaultnord
diff --git a/mail/email/invite_confirmed.html.hbs b/mail/email/invite_confirmed.html.hbs
deleted file mode 100644
index 5a43e10..0000000
--- a/mail/email/invite_confirmed.html.hbs
+++ /dev/null
@@ -1,20 +0,0 @@
-Du har fått tilgang til {{{org_name}}}
-
-
-
-
-
-
- | vaultnord |
-
-Du har fått tilgang til {{org_name}}
- Du er nå bekreftet som medlem, og passordene organisasjonen deler med deg dukker opp i hvelvet ditt.
- Bruker du appen eller nettleserutvidelsen, kan du måtte synkronisere én gang for å se dem.
- |
- |
- Vaultnord · drift i Finland, backup i Tyskland — alt innenfor EU.
- |
-
- |
-
-
diff --git a/mail/email/new_device_logged_in.hbs b/mail/email/new_device_logged_in.hbs
deleted file mode 100644
index 4c13e35..0000000
--- a/mail/email/new_device_logged_in.hbs
+++ /dev/null
@@ -1,16 +0,0 @@
-Ny innlogging fra {{{device_name}}}
-
-Hei,
-
-Kontoen din ble nettopp brukt til å logge inn fra en ny enhet.
-
- Tidspunkt: {{datetime}}
- IP-adresse: {{ip}}
- Enhet: {{device_name}} ({{device_type}})
-
-Var det deg, trenger du ikke gjøre noe.
-
-Kjenner du deg ikke igjen: bytt hovedpassord med en gang, og logg ut alle
-enheter under Innstillinger i hvelvet ({{url}}).
-
-Vaultnord
diff --git a/mail/email/new_device_logged_in.html.hbs b/mail/email/new_device_logged_in.html.hbs
deleted file mode 100644
index dec3ccd..0000000
--- a/mail/email/new_device_logged_in.html.hbs
+++ /dev/null
@@ -1,25 +0,0 @@
-Ny innlogging fra {{{device_name}}}
-
-
-
-
-
-
- | vaultnord |
-
- Ny innlogging på kontoen din
- Kontoen din ble nettopp brukt til å logge inn fra en ny enhet.
-
- | Tidspunkt | {{datetime}} |
- | IP-adresse | {{ip}} |
- | Enhet | {{device_name}} ({{device_type}}) |
-
- Var det deg, trenger du ikke gjøre noe. Kjenner du deg ikke igjen: bytt hovedpassord med en gang, og logg ut alle enheter under Innstillinger i hvelvet.
- |
- |
- Vaultnord · drift i Finland, backup i Tyskland — alt innenfor EU.
- |
-
- |
-
-
diff --git a/mail/email/send_org_invite.hbs b/mail/email/send_org_invite.hbs
deleted file mode 100644
index 38b6aac..0000000
--- a/mail/email/send_org_invite.hbs
+++ /dev/null
@@ -1,18 +0,0 @@
-Invitasjon til passordhvelvet hos {{{org_name}}}
-
-Hei,
-
-Du er invitert til passordhvelvet til {{org_name}}.
-
-Åpne denne lenken for å komme i gang. Der setter du ditt eget hovedpassord —
-det er bare du som kjenner det, og det kan ikke gjenopprettes av noen andre:
-
-{{{url}}}
-
-Lenken er gyldig i fem dager. Bruk hele adressen, også den lange tegnrekken
-til slutt.
-
-Var ikke dette ventet? Da kan du se bort fra e-posten.
-
-Vaultnord
-Passordhvelv driftet i Norden
diff --git a/mail/email/send_org_invite.html.hbs b/mail/email/send_org_invite.html.hbs
deleted file mode 100644
index dff2b00..0000000
--- a/mail/email/send_org_invite.html.hbs
+++ /dev/null
@@ -1,29 +0,0 @@
-Invitasjon til passordhvelvet hos {{{org_name}}}
-
-
-
-
-
-
- | vaultnord |
-
- Du er invitert til passordhvelvet hos {{org_name}}
- Åpne lenken under for å komme i gang. Der setter du ditt eget hovedpassord — det er bare du som kjenner det, og det kan ikke gjenopprettes av noen andre.
- |
- |
-
- |
- |
- Lenken er gyldig i fem dager. Var ikke dette ventet, kan du se bort fra e-posten.
- |
- |
- Vaultnord · passordhvelv driftet i Norden
- |
-
- |
-
-
diff --git a/mail/email/twofactor_email.hbs b/mail/email/twofactor_email.hbs
deleted file mode 100644
index a73c58a..0000000
--- a/mail/email/twofactor_email.hbs
+++ /dev/null
@@ -1,9 +0,0 @@
-Innloggingskode: {{{token}}}
-
-Koden din er: {{token}}
-
-Bruk den for å fullføre innloggingen. Den varer i noen få minutter.
-
-Var det ikke du som logget inn, bør du bytte hovedpassord.
-
-Vaultnord
diff --git a/mail/email/twofactor_email.html.hbs b/mail/email/twofactor_email.html.hbs
deleted file mode 100644
index f56fe55..0000000
--- a/mail/email/twofactor_email.html.hbs
+++ /dev/null
@@ -1,21 +0,0 @@
-Innloggingskode: {{{token}}}
-
-
-
-
-
-
- | vaultnord |
-
-Innloggingskode
- Koden din er:
- {{token}}
- Den varer i noen få minutter. Var det ikke du som logget inn, bør du bytte hovedpassord.
- |
- |
- Vaultnord · drift i Finland, backup i Tyskland — alt innenfor EU.
- |
-
- |
-
-
diff --git a/mail/email/verify_email.hbs b/mail/email/verify_email.hbs
deleted file mode 100644
index 1fcf716..0000000
--- a/mail/email/verify_email.hbs
+++ /dev/null
@@ -1,9 +0,0 @@
-Bekreft e-postadressen din
-
-Bekreft e-postadressen for kontoen din ved å åpne lenken under:
-
-{{url}}/#/verify-email/?userId={{user_id}}&token={{token}}
-
-Var det ikke du som ba om dette, kan du se bort fra e-posten.
-
-Vaultnord
diff --git a/mail/email/verify_email.html.hbs b/mail/email/verify_email.html.hbs
deleted file mode 100644
index 6fbff97..0000000
--- a/mail/email/verify_email.html.hbs
+++ /dev/null
@@ -1,24 +0,0 @@
-Bekreft e-postadressen din
-
-
-
-
-
-
- | vaultnord |
-
-Bekreft e-postadressen din
- Åpne lenken under for å bekrefte adressen for kontoen din.
-
- Var det ikke du som ba om dette, kan du se bort fra e-posten.
- |
- |
- Vaultnord · drift i Finland, backup i Tyskland — alt innenfor EU.
- |
-
- |
-
-
diff --git a/mail/email/welcome.hbs b/mail/email/welcome.hbs
deleted file mode 100644
index 88d2e1b..0000000
--- a/mail/email/welcome.hbs
+++ /dev/null
@@ -1,11 +0,0 @@
-Hvelvet ditt er klart til bruk
-
-Hei,
-
-Hovedpassordet ditt er satt, og hvelvet er klart: {{url}}
-
-Ett tips til slutt: installer Bitwarden-appen eller nettleserutvidelsen og oppgi
-adressen over som serveradresse. Nettleserversjonen låser seg hver gang siden
-lastes på nytt, fordi nøkkelen bare finnes i minnet - utvidelsen holder økten.
-
-Vaultnord
diff --git a/mail/email/welcome.html.hbs b/mail/email/welcome.html.hbs
deleted file mode 100644
index 13f5aab..0000000
--- a/mail/email/welcome.html.hbs
+++ /dev/null
@@ -1,20 +0,0 @@
-Hvelvet ditt er klart til bruk
-
-
-
-
-
-
- | vaultnord |
-
- Hvelvet ditt er klart
- Hovedpassordet ditt er satt, og hvelvet er klart til bruk på {{url}}.
- Ett tips: installer Bitwarden-appen eller nettleserutvidelsen og oppgi adressen over som serveradresse. Nettleserversjonen låser seg hver gang siden lastes på nytt, fordi nøkkelen bare finnes i minnet — utvidelsen holder økten.
- |
- |
- Vaultnord · drift i Finland, backup i Tyskland — alt innenfor EU.
- |
-
- |
-
-
diff --git a/mail/maler.py b/mail/maler.py
new file mode 100644
index 0000000..f4a66b6
--- /dev/null
+++ b/mail/maler.py
@@ -0,0 +1,599 @@
+"""Bygger e-postmalene til hvelvet, ett sett per språk.
+
+Vaultwarden har 30 maler. Vi hadde skrevet sju av dem om til norsk; resten gikk
+ut på engelsk med Bitwardens navn i teksten - altså midt i onboardingen. Å
+håndskrive de 23 siste i to formater og to språk blir 92 filer som må holdes i
+takt, så teksten ligger heller i tabellen under og formene genereres.
+
+Kjøres under bygget: python3 maler.py
+og legger igjen //email/.hbs og .html.hbs.
+
+Et nytt språk er en kolonne til i tabellen, ikke en ny mappe med filer.
+"""
+
+import re
+import sys
+import textwrap
+from pathlib import Path
+
+SPRÅK = ("nb", "en")
+
+# Bunnteksten under strekend i hver e-post.
+BUNN = {
+ "nb": "Vaultnord · drift i Finland, backup i Tyskland — alt innenfor EU.",
+ "en": "Vaultnord · hosted in Finland, backed up in Germany — all within the EU.",
+}
+BUNN_TEKST = {"nb": "Vaultnord", "en": "Vaultnord"}
+
+# Navnet på appen kunden faktisk installerer. Den heter Bitwarden i butikkene,
+# og å kalle den noe annet ville sende folk på et umulig søk.
+APP = "Bitwarden"
+
+
+# --- oppmerking ------------------------------------------------------------
+# Teksten skrives én gang. [tekst](lenke) blir en lenke, *ord* blir uthevet, og
+# linjer som begynner med "- " blir en liste. Alt annet er avsnitt.
+
+LENKE = re.compile(r"\[([^\]]+)\]\(([^)]+)\)")
+UTHEVET = re.compile(r"\*([^*]+)\*")
+
+
+BARE_LENKE = re.compile(r"^\[([^\]]+)\]\(([^)]+)\)$")
+
+
+def _tekst(avsnitt: list[str]) -> str:
+ ut = []
+ for a in avsnitt:
+ if a.startswith("- "):
+ ut.append("\n".join(" " + linje[2:] for linje in a.split("\n")))
+ continue
+ # En lenke som står alene er en oppfordring: sett adressen på egen linje,
+ # uinnpakket, så den blir klikkbar i alle klienter.
+ alene = BARE_LENKE.match(a)
+ if alene:
+ ut.append(f"{alene.group(1)}:\n{alene.group(2)}")
+ continue
+ ut.append(textwrap.fill(LENKE.sub(_flat, a), 78,
+ break_long_words=False, break_on_hyphens=False))
+ return "\n\n".join(ut)
+
+
+def _flat(m: re.Match) -> str:
+ """Lenke midt i en setning. Er teksten adressen selv, skal den ikke stå to
+ ganger - da ble det «{{url}}: {{{url}}}» i innboksen."""
+ tekst, adresse = m.group(1), m.group(2)
+ if tekst.strip("{} ") == adresse.strip("{} "):
+ return tekst
+ return f"{tekst} ({adresse})"
+
+
+def _html(avsnitt: list[str]) -> str:
+ p = ('margin:0 0 16px;color:#2e3440;font-size:15px;line-height:1.6')
+ ut = []
+ for a in avsnitt:
+ if a.startswith("- "):
+ punkter = "".join(
+ f'{_inline(linje[2:])}'
+ for linje in a.split("\n"))
+ ut.append(f'')
+ else:
+ ut.append(f'{_inline(a)}
')
+ return "\n ".join(ut)
+
+
+def _inline(s: str) -> str:
+ s = LENKE.sub(r'\1', s)
+ return UTHEVET.sub(r"\1", s)
+
+
+RAMME = """
+
+
+
+
+ | vaultnord |
+
+ {tittel}
+ {kropp}
+ |
+ |
+ {bunn}
+ |
+
+ |
+
+"""
+
+HTML_LANG = {"nb": "no", "en": "en"}
+
+
+# --- teksten ---------------------------------------------------------------
+# Per mal og språk: (emne, overskrift, avsnitt). Variabelnavnene er
+# Vaultwardens egne - endres de her, kommer e-posten ut med tomme felter.
+
+MALER: dict[str, dict[str, tuple[str, str, list[str]]]] = {
+
+"welcome": {
+ "nb": ("Hvelvet ditt er klart til bruk", "Hvelvet ditt er klart", [
+ "Hovedpassordet ditt er satt, og hvelvet er klart til bruk på [{{url}}]({{{url}}}).",
+ f"Ett tips til slutt: installer {APP}-appen eller nettleserutvidelsen og oppgi "
+ "adressen over som serveradresse. Nettleserversjonen låser seg hver gang siden "
+ "lastes på nytt, fordi nøkkelen bare finnes i minnet — utvidelsen holder økten.",
+ ]),
+ "en": ("Your vault is ready", "Your vault is ready", [
+ "Your master password is set, and your vault is ready at [{{url}}]({{{url}}}).",
+ f"One tip: install the {APP} app or browser extension and enter the address "
+ "above as the server address. The web version locks every time the page "
+ "reloads, because the key only lives in memory — the extension keeps the session.",
+ ]),
+},
+
+"welcome_must_verify": {
+ "nb": ("Bekreft e-postadressen din", "Ett steg igjen", [
+ "Kontoen din på [{{url}}]({{{url}}}) er opprettet. Før du kan logge inn må du "
+ "bekrefte at adressen er din.",
+ "[Bekreft e-postadressen]({{url}}/#/verify-email/?userId={{user_id}}&token={{token}})",
+ "Var det ikke du som opprettet kontoen, kan du se bort fra e-posten.",
+ ]),
+ "en": ("Verify your email address", "One step left", [
+ "Your account at [{{url}}]({{{url}}}) has been created. Before you can log in, "
+ "you need to confirm that this address is yours.",
+ "[Verify your email address]({{url}}/#/verify-email/?userId={{user_id}}&token={{token}})",
+ "If you did not create this account, you can ignore this email.",
+ ]),
+},
+
+"verify_email": {
+ "nb": ("Bekreft e-postadressen din", "Bekreft e-postadressen", [
+ "Bekreft e-postadressen for kontoen din ved å åpne lenken under:",
+ "[Bekreft e-postadressen]({{url}}/#/verify-email/?userId={{user_id}}&token={{token}})",
+ "Var det ikke du som ba om dette, kan du se bort fra e-posten.",
+ ]),
+ "en": ("Verify your email address", "Verify your email address", [
+ "Confirm the email address on your account by opening the link below:",
+ "[Verify your email address]({{url}}/#/verify-email/?userId={{user_id}}&token={{token}})",
+ "If you did not ask for this, you can ignore this email.",
+ ]),
+},
+
+"register_verify_email": {
+ "nb": ("Bekreft e-postadressen din", "Bekreft e-postadressen", [
+ "Bekreft adressen for å fullføre opprettelsen av kontoen.",
+ "[Bekreft e-postadressen]({{{url}}})",
+ "Var det ikke du som ba om dette, kan du se bort fra e-posten.",
+ ]),
+ "en": ("Verify your email address", "Verify your email address", [
+ "Confirm this address to finish creating your account.",
+ "[Verify your email address]({{{url}}})",
+ "If you did not ask for this, you can ignore this email.",
+ ]),
+},
+
+"send_org_invite": {
+ "nb": ("Invitasjon til passordhvelvet hos {{{org_name}}}", "Du er invitert", [
+ "Du er invitert til passordhvelvet til {{org_name}}.",
+ "Åpne lenken under for å komme i gang. Der setter du ditt eget hovedpassord — "
+ "det er bare du som kjenner det, og det kan ikke gjenopprettes av noen andre.",
+ "[Bli med]({{{url}}})",
+ "Lenken er gyldig i fem dager. Bruk hele adressen, også den lange tegnrekken til slutt.",
+ "Var ikke dette ventet? Da kan du se bort fra e-posten.",
+ ]),
+ "en": ("Invitation to the password vault at {{{org_name}}}", "You have been invited", [
+ "You have been invited to the password vault of {{org_name}}.",
+ "Open the link below to get started. There you set your own master password — "
+ "you are the only one who knows it, and nobody else can recover it.",
+ "[Join]({{{url}}})",
+ "The link is valid for five days. Use the whole address, including the long string at the end.",
+ "Were you not expecting this? Then you can ignore this email.",
+ ]),
+},
+
+"invite_accepted": {
+ "nb": ("{{{email}}} har takket ja til invitasjonen", "Én bekreftelse gjenstår", [
+ "{{email}} har satt hovedpassordet sitt og takket ja til invitasjonen til {{org_name}}.",
+ "Siste steg må gjøres av deg: logg inn på [{{url}}]({{{url}}}) og bekreft brukeren "
+ "under Medlemmer i organisasjonen. Først da får de tilgang til det som deles.",
+ "Bekreftelsen kan bare gjøres av deg — den krever organisasjonsnøkkelen, som bare "
+ "finnes i din egen innloggede klient. Det er derfor vi ikke kan gjøre det for deg.",
+ ]),
+ "en": ("{{{email}}} accepted the invitation", "One confirmation left", [
+ "{{email}} has set their master password and accepted the invitation to {{org_name}}.",
+ "The last step is yours: log in at [{{url}}]({{{url}}}) and confirm the user under "
+ "Members in the organisation. Only then do they get access to what is shared.",
+ "Only you can confirm — it requires the organisation key, which exists solely in "
+ "your own logged-in client. That is why we cannot do it for you.",
+ ]),
+},
+
+"invite_confirmed": {
+ "nb": ("Du har fått tilgang til {{{org_name}}}", "Du er med", [
+ "Du er nå bekreftet som medlem av {{org_name}}.",
+ "Passordene organisasjonen deler med deg dukker opp i hvelvet ditt: [{{url}}]({{{url}}}).",
+ f"Bruker du {APP}-appen eller nettleserutvidelsen, kan du måtte synkronisere en gang for å se dem.",
+ ]),
+ "en": ("You now have access to {{{org_name}}}", "You are in", [
+ "You have been confirmed as a member of {{org_name}}.",
+ "The passwords the organisation shares with you appear in your vault: [{{url}}]({{{url}}}).",
+ f"If you use the {APP} app or browser extension, you may need to sync once to see them.",
+ ]),
+},
+
+"new_device_logged_in": {
+ "nb": ("Ny innlogging fra {{{device_name}}}", "Ny innlogging", [
+ "Kontoen din ble nettopp brukt til å logge inn fra en ny enhet.",
+ "- Tidspunkt: {{datetime}}\n- IP-adresse: {{ip}}\n- Enhet: {{device_name}} ({{device_type}})",
+ "Var det deg, trenger du ikke gjøre noe.",
+ "Kjenner du deg ikke igjen: bytt hovedpassord med en gang, og logg ut alle enheter "
+ "under Innstillinger i hvelvet ([{{url}}]({{{url}}})).",
+ ]),
+ "en": ("New login from {{{device_name}}}", "New login", [
+ "Your account was just used to log in from a new device.",
+ "- Time: {{datetime}}\n- IP address: {{ip}}\n- Device: {{device_name}} ({{device_type}})",
+ "If this was you, there is nothing to do.",
+ "If you do not recognise it: change your master password right away, and sign out "
+ "all devices under Settings in the vault ([{{url}}]({{{url}}})).",
+ ]),
+},
+
+"incomplete_2fa_login": {
+ "nb": ("Avbrutt innlogging fra {{{device_name}}}", "Noen kom halvveis inn", [
+ "Noen logget inn på kontoen din med riktig hovedpassord, men fullførte ikke "
+ "totrinnsbekreftelsen innen {{time_limit}} minutter.",
+ "- Tidspunkt: {{datetime}}\n- IP-adresse: {{ip}}\n- Enhet: {{device_name}} ({{device_type}})",
+ "Var det ikke du eller noen du kjenner: *bytt hovedpassord med en gang*. At passordet "
+ "var riktig betyr at det er på avveie.",
+ ]),
+ "en": ("Interrupted login from {{{device_name}}}", "Someone got halfway in", [
+ "Someone logged into your account with the correct master password, but did not "
+ "complete two-step verification within {{time_limit}} minutes.",
+ "- Time: {{datetime}}\n- IP address: {{ip}}\n- Device: {{device_name}} ({{device_type}})",
+ "If this was not you or someone you know: *change your master password immediately*. "
+ "The password being correct means it has leaked.",
+ ]),
+},
+
+"twofactor_email": {
+ "nb": ("Innloggingskode: {{{token}}}", "Innloggingskode", [
+ "Koden din er: *{{token}}*",
+ "Bruk den for å fullføre innloggingen. Den varer i noen få minutter.",
+ "Var det ikke du som logget inn, bør du bytte hovedpassord.",
+ ]),
+ "en": ("Login code: {{{token}}}", "Login code", [
+ "Your code is: *{{token}}*",
+ "Use it to finish logging in. It lasts a few minutes.",
+ "If you were not the one logging in, you should change your master password.",
+ ]),
+},
+
+"protected_action": {
+ "nb": ("Bekreftelseskode: {{{token}}}", "Bekreftelseskode", [
+ "Koden din er: *{{token}}*",
+ "Bruk den for å fullføre handlingen du startet i hvelvet.",
+ ]),
+ "en": ("Verification code: {{{token}}}", "Verification code", [
+ "Your code is: *{{token}}*",
+ "Use it to complete the action you started in the vault.",
+ ]),
+},
+
+"change_email": {
+ "nb": ("Bytte av e-postadresse", "Bekreft den nye adressen", [
+ "Skriv denne koden i hvelvet for å fullføre byttet av e-postadresse: *{{token}}*",
+ "Var det ikke du som ba om byttet, ta kontakt med administratoren din.",
+ ]),
+ "en": ("Email address change", "Confirm the new address", [
+ "Enter this code in the vault to finish changing your email address: *{{token}}*",
+ "If you did not ask for the change, contact your administrator.",
+ ]),
+},
+
+"change_email_existing": {
+ "nb": ("Noen forsøkte å bruke adressen din", "Adressen er allerede i bruk", [
+ "En bruker ({{ acting_address }}) forsøkte nettopp å flytte kontoen sin til denne "
+ "adressen ({{ existing_address }}). Det finnes allerede en konto her.",
+ "Var det ikke du som forsøkte, ta kontakt med administratoren din.",
+ ]),
+ "en": ("Someone tried to use your address", "The address is already in use", [
+ "A user ({{ acting_address }}) just tried to move their account to this address "
+ "({{ existing_address }}). An account already exists here.",
+ "If it was not you, contact your administrator.",
+ ]),
+},
+
+"change_email_invited": {
+ "nb": ("Noen forsøkte å bruke adressen din", "Adressen er allerede invitert", [
+ "En bruker ({{ acting_address }}) forsøkte nettopp å flytte kontoen sin til denne "
+ "adressen ({{ existing_address }}). Adressen er allerede invitert til et hvelv.",
+ "Skal adressen brukes til noe annet, må kontoen som hører til den slettes først: "
+ "[be om sletting]({{url}}/#/recover-delete). Invitasjonen må da sendes på nytt.",
+ "Var det ikke du som forsøkte, ta kontakt med administratoren din.",
+ ]),
+ "en": ("Someone tried to use your address", "The address is already invited", [
+ "A user ({{ acting_address }}) just tried to move their account to this address "
+ "({{ existing_address }}). The address has already been invited to a vault.",
+ "To use the address for something else, the account tied to it must be deleted "
+ "first: [request deletion]({{url}}/#/recover-delete). The invitation then has to be resent.",
+ "If it was not you, contact your administrator.",
+ ]),
+},
+
+"delete_account": {
+ "nb": ("Slett kontoen din", "Bekreft sletting", [
+ "Åpne lenken under for å slette kontoen din. *Alt innholdet i hvelvet forsvinner, "
+ "og det kan ikke hentes tilbake.*",
+ "[Slett kontoen]({{url}}/#/verify-recover-delete?userId={{user_id}}&token={{token}}&email={{email}})",
+ "Var det ikke du som ba om dette, kan du se bort fra e-posten.",
+ ]),
+ "en": ("Delete your account", "Confirm deletion", [
+ "Open the link below to delete your account. *Everything in the vault is lost, "
+ "and it cannot be brought back.*",
+ "[Delete the account]({{url}}/#/verify-recover-delete?userId={{user_id}}&token={{token}}&email={{email}})",
+ "If you did not ask for this, you can ignore this email.",
+ ]),
+},
+
+"pw_hint_some": {
+ "nb": ("Hintet til hovedpassordet ditt", "Hintet ditt", [
+ "Noen ba om hintet til hovedpassordet ditt.",
+ "Hintet er: *{{hint}}*",
+ "[Logg inn i hvelvet]({{{url}}})",
+ "Husker du ikke hovedpassordet, finnes det ingen vei tilbake til innholdet — det er "
+ "kryptert med nøkkelen bare du har. Eneste utvei er å [slette kontoen]({{url}}/#/recover-delete) "
+ "og begynne på nytt. Alt innholdet forsvinner da.",
+ "Var det ikke du som ba om hintet, kan du se bort fra e-posten.",
+ ]),
+ "en": ("Your master password hint", "Your hint", [
+ "Someone asked for your master password hint.",
+ "The hint is: *{{hint}}*",
+ "[Log in to the vault]({{{url}}})",
+ "If you cannot remember your master password, there is no way back to the contents — "
+ "they are encrypted with a key only you hold. The only way out is to "
+ "[delete the account]({{url}}/#/recover-delete) and start over. Everything in it is lost.",
+ "If you did not ask for the hint, you can ignore this email.",
+ ]),
+},
+
+"pw_hint_none": {
+ "nb": ("Hintet til hovedpassordet ditt", "Du har ikke lagret noe hint", [
+ "Noen ba om hintet til hovedpassordet ditt, men kontoen din har ikke noe hint lagret.",
+ "Husker du ikke hovedpassordet, finnes det ingen vei tilbake til innholdet — det er "
+ "kryptert med nøkkelen bare du har. Eneste utvei er å [slette kontoen]({{url}}/#/recover-delete) "
+ "og begynne på nytt. Alt innholdet forsvinner da.",
+ "Var det ikke du som ba om hintet, kan du se bort fra e-posten.",
+ ]),
+ "en": ("Your master password hint", "You have no hint saved", [
+ "Someone asked for your master password hint, but your account has none saved.",
+ "If you cannot remember your master password, there is no way back to the contents — "
+ "they are encrypted with a key only you hold. The only way out is to "
+ "[delete the account]({{url}}/#/recover-delete) and start over. Everything in it is lost.",
+ "If you did not ask for the hint, you can ignore this email.",
+ ]),
+},
+
+"admin_reset_password": {
+ "nb": ("Hovedpassordet ditt er endret", "Hovedpassordet er endret", [
+ "Hovedpassordet til {{user_name}} er endret av en administrator i {{org_name}}.",
+ "Var det ikke avtalt, ta kontakt med administratoren din med en gang.",
+ ]),
+ "en": ("Your master password has been changed", "Master password changed", [
+ "The master password for {{user_name}} was changed by an administrator in {{org_name}}.",
+ "If this was not agreed, contact your administrator immediately.",
+ ]),
+},
+
+"send_2fa_removed_from_org": {
+ "nb": ("Tilgangen din til {{{org_name}}} er fjernet", "Tilgangen er fjernet", [
+ "Du er tatt ut av {{org_name}} fordi kontoen din ikke har totrinnsbekreftelse.",
+ "Slå på totrinnsbekreftelse under Innstillinger i hvelvet, så kan du bli med igjen.",
+ ]),
+ "en": ("Your access to {{{org_name}}} has been removed", "Access removed", [
+ "You were removed from {{org_name}} because your account has no two-step verification.",
+ "Turn on two-step verification under Settings in the vault, and you can rejoin.",
+ ]),
+},
+
+"send_single_org_removed_from_org": {
+ "nb": ("Tilgangen din til {{{org_name}}} er fjernet", "Tilgangen er fjernet", [
+ "Du er tatt ut av {{org_name}} fordi du også er medlem av en annen organisasjon. "
+ "{{org_name}} tillater ikke det.",
+ "Meld deg ut av de andre organisasjonene, eller bli med med en annen konto.",
+ ]),
+ "en": ("Your access to {{{org_name}}} has been removed", "Access removed", [
+ "You were removed from {{org_name}} because you are also a member of another "
+ "organisation. {{org_name}} does not allow that.",
+ "Leave the other organisations, or join with a different account.",
+ ]),
+},
+
+"sso_change_email": {
+ "nb": ("E-postadressen din er endret", "Adressen er endret hos påloggingstjenesten", [
+ "E-postadressen din ble endret hos påloggingstjenesten. Oppdater den også i hvelvet, "
+ "under Kontoinnstillinger: [{{url}}]({{{url}}}).",
+ ]),
+ "en": ("Your email address has changed", "The address changed at your login provider", [
+ "Your email address was changed at your login provider. Update it in the vault too, "
+ "under Account settings: [{{url}}]({{{url}}}).",
+ ]),
+},
+
+"smtp_test": {
+ "nb": ("Test av e-postoppsettet", "Testen kom fram", [
+ "Dette er en testmelding for e-postoppsettet til {{url}}.",
+ "Kan du lese den, virker oppsettet.",
+ ]),
+ "en": ("Email configuration test", "The test arrived", [
+ "This is a test message for the email setup of {{url}}.",
+ "If you can read it, the setup works.",
+ ]),
+},
+
+"send_emergency_access_invite": {
+ "nb": ("Nødtilgang for {{{grantor_name}}}", "Du er spurt om å være nødkontakt", [
+ "{{grantor_name}} har spurt om du vil være nødkontakt for hvelvet sitt.",
+ "[Ta imot]({{{url}}})",
+ "Vil du ikke, kan du se bort fra e-posten.",
+ ]),
+ "en": ("Emergency access for {{{grantor_name}}}", "You have been asked to be an emergency contact", [
+ "{{grantor_name}} has asked you to be an emergency contact for their vault.",
+ "[Accept]({{{url}}})",
+ "If you would rather not, you can ignore this email.",
+ ]),
+},
+
+"emergency_access_invite_accepted": {
+ "nb": ("{{{grantee_email}}} har takket ja", "Én bekreftelse gjenstår", [
+ "{{grantee_email}} har takket ja til å være nødkontakten din.",
+ "Logg inn i hvelvet ([{{url}}]({{{url}}})) og bekreft dem under Innstillinger. "
+ "Vil du likevel ikke, kan du fjerne dem samme sted.",
+ ]),
+ "en": ("{{{grantee_email}}} accepted", "One confirmation left", [
+ "{{grantee_email}} has accepted being your emergency contact.",
+ "Log in to the vault ([{{url}}]({{{url}}})) and confirm them under Settings. "
+ "If you have changed your mind, you can remove them in the same place.",
+ ]),
+},
+
+"emergency_access_invite_confirmed": {
+ "nb": ("Du er nødkontakt for {{{grantor_name}}}", "Du er bekreftet", [
+ "Du er bekreftet som nødkontakt for *{{grantor_name}}*.",
+ "Du kan be om nødtilgang fra hvelvet: [{{url}}]({{{url}}}).",
+ ]),
+ "en": ("You are an emergency contact for {{{grantor_name}}}", "You are confirmed", [
+ "You have been confirmed as an emergency contact for *{{grantor_name}}*.",
+ "You can request emergency access from the vault: [{{url}}]({{{url}}}).",
+ ]),
+},
+
+"emergency_access_recovery_initiated": {
+ "nb": ("{{{grantee_name}}} har bedt om nødtilgang", "Noen ber om nødtilgang", [
+ "{{grantee_name}} har bedt om nødtilgang for å {{atype}} kontoen din.",
+ "Logg inn i hvelvet ([{{url}}]({{{url}}})) for å godta eller avslå.",
+ "Gjør du ingenting, blir forespørselen *godtatt automatisk om {{wait_time_days}} dager*.",
+ ]),
+ "en": ("{{{grantee_name}}} requested emergency access", "Someone is requesting emergency access", [
+ "{{grantee_name}} has requested emergency access to {{atype}} your account.",
+ "Log in to the vault ([{{url}}]({{{url}}})) to approve or reject it.",
+ "If you do nothing, the request is *approved automatically in {{wait_time_days}} days*.",
+ ]),
+},
+
+"emergency_access_recovery_reminder": {
+ "nb": ("{{{grantee_name}}} venter fortsatt på nødtilgang", "Forespørselen ligger fortsatt der", [
+ "{{grantee_name}} har en ubesvart forespørsel om nødtilgang for å {{atype}} kontoen din.",
+ "Logg inn i hvelvet ([{{url}}]({{{url}}})) for å godta eller avslå.",
+ "Gjør du ingenting, blir forespørselen *godtatt automatisk om {{days_left}} dager*.",
+ ]),
+ "en": ("{{{grantee_name}}} is still waiting for emergency access", "The request is still open", [
+ "{{grantee_name}} has a pending emergency access request to {{atype}} your account.",
+ "Log in to the vault ([{{url}}]({{{url}}})) to approve or reject it.",
+ "If you do nothing, the request is *approved automatically in {{days_left}} days*.",
+ ]),
+},
+
+"emergency_access_recovery_approved": {
+ "nb": ("Nødtilgangen din er godtatt", "Godtatt", [
+ "{{grantor_name}} har godtatt forespørselen din om nødtilgang.",
+ "Du kan nå logge inn i hvelvet: [{{url}}]({{{url}}}).",
+ ]),
+ "en": ("Your emergency access was approved", "Approved", [
+ "{{grantor_name}} approved your emergency access request.",
+ "You can now log in to the vault: [{{url}}]({{{url}}}).",
+ ]),
+},
+
+"emergency_access_recovery_rejected": {
+ "nb": ("Nødtilgangen din er avslått", "Avslått", [
+ "{{grantor_name}} har avslått forespørselen din om nødtilgang.",
+ ]),
+ "en": ("Your emergency access was rejected", "Rejected", [
+ "{{grantor_name}} rejected your emergency access request.",
+ ]),
+},
+
+"emergency_access_recovery_timed_out": {
+ "nb": ("{{{grantee_name}}} har fått nødtilgang", "Fristen gikk ut", [
+ "{{grantee_name}} har fått nødtilgang til å {{atype}} kontoen din, fordi forespørselen "
+ "ble stående ubesvart til fristen gikk ut.",
+ "Var ikke dette meningen, kan du trekke tilgangen tilbake i hvelvet: [{{url}}]({{{url}}}).",
+ ]),
+ "en": ("{{{grantee_name}}} was granted emergency access", "The deadline passed", [
+ "{{grantee_name}} has been granted emergency access to {{atype}} your account, because "
+ "the request went unanswered until the deadline passed.",
+ "If that was not intended, you can revoke the access in the vault: [{{url}}]({{{url}}}).",
+ ]),
+},
+
+}
+
+
+def mot_binær(sti: Path) -> None:
+ """Sammenlign med malene serveren faktisk kan sende.
+
+ Navnene ligger som tekst i binæren. Kommer det en ny mal i en oppgradering,
+ ville den gått ut med serverens innebygde engelske tekst - og den sier
+ Bitwarden. Da skal bygget stoppe her i stedet for at en kunde oppdager det
+ i innboksen."""
+ data = sti.read_bytes()
+ finnes = {m.group(1).decode()
+ for m in re.finditer(rb"email/([a-z0-9_]+)(?=[A-Z{])", data)}
+ # Partialene har ingen egen tekst; de innebygde malene bruker dem, våre ikke.
+ finnes -= {"email_header", "email_footer", "email_footer_text"}
+ mangler = finnes - set(MALER)
+ if mangler:
+ raise SystemExit("maler.py: serveren har maler vi ikke har skrevet: "
+ + ", ".join(sorted(mangler)))
+
+
+def skriv(ut: Path) -> int:
+ n = 0
+ for navn, per_språk in MALER.items():
+ for språk, (emne, tittel, avsnitt) in per_språk.items():
+ mappe = ut / språk / "email"
+ mappe.mkdir(parents=True, exist_ok=True)
+ skille = ""
+ (mappe / f"{navn}.hbs").write_text(
+ f"{emne}\n{skille}\n{_tekst(avsnitt)}\n\n{BUNN_TEKST[språk]}\n",
+ encoding="utf-8")
+ (mappe / f"{navn}.html.hbs").write_text(
+ f"{emne}\n{skille}\n" + RAMME.format(
+ lang=HTML_LANG[språk], tittel=tittel,
+ kropp=_html(avsnitt), bunn=BUNN[språk]) + "\n",
+ encoding="utf-8")
+ n += 2
+ return n
+
+
+def sjekk() -> None:
+ feil = []
+ for navn, per_språk in MALER.items():
+ if set(per_språk) != set(SPRÅK):
+ feil.append(f"{navn}: mangler språk {set(SPRÅK) - set(per_språk)}")
+ for språk, (emne, tittel, avsnitt) in per_språk.items():
+ alt = " ".join([emne, tittel, *avsnitt])
+ # Vaultwardens eget navn skal aldri stå i en e-post fra oss.
+ if "vaultwarden" in alt.lower():
+ feil.append(f"{navn}/{språk}: sier Vaultwarden")
+ # Bitwarden er navnet på appen kunden installerer, og skal bare
+ # stå der vi faktisk viser til appen eller utvidelsen.
+ for treff in re.finditer(r"Bitwarden(\S*)", alt):
+ if treff.group(1) not in ("-appen", "-utvidelsen", ""):
+ feil.append(f"{navn}/{språk}: uventet bruk av Bitwarden")
+ if alt.count("{{") != alt.count("}}"):
+ feil.append(f"{navn}/{språk}: ubalanserte klammer")
+ if not avsnitt:
+ feil.append(f"{navn}/{språk}: ingen tekst")
+ if feil:
+ raise SystemExit("maler.py: " + "; ".join(feil))
+
+
+if __name__ == "__main__":
+ sjekk()
+ if len(sys.argv) > 2 and sys.argv[1] == "--mot":
+ mot_binær(Path(sys.argv[2]))
+ print("maler.py: alle malene serveren kan sende er dekket")
+ raise SystemExit(0)
+ if len(sys.argv) > 1:
+ antall = skriv(Path(sys.argv[1]))
+ print(f"maler.py: skrev {antall} filer i {len(SPRÅK)} språk")
+ else:
+ print(f"maler.py: {len(MALER)} maler, {len(SPRÅK)} språk - alt i orden")