From 6a7315ba97b35b6b4338988f58a27c00c8163a42 Mon Sep 17 00:00:00 2001 From: Vaultnord Date: Mon, 31 Aug 2026 21:40:43 +0000 Subject: [PATCH] Synk fra arbeidsrepoet --- Dockerfile | 15 +- mail/email/invite_accepted.hbs | 12 - mail/email/invite_accepted.html.hbs | 21 - mail/email/invite_confirmed.hbs | 10 - mail/email/invite_confirmed.html.hbs | 20 - mail/email/new_device_logged_in.hbs | 16 - mail/email/new_device_logged_in.html.hbs | 25 - mail/email/send_org_invite.hbs | 18 - mail/email/send_org_invite.html.hbs | 29 -- mail/email/twofactor_email.hbs | 9 - mail/email/twofactor_email.html.hbs | 21 - mail/email/verify_email.hbs | 9 - mail/email/verify_email.html.hbs | 24 - mail/email/welcome.hbs | 11 - mail/email/welcome.html.hbs | 20 - mail/maler.py | 599 +++++++++++++++++++++++ 16 files changed, 612 insertions(+), 247 deletions(-) delete mode 100644 mail/email/invite_accepted.hbs delete mode 100644 mail/email/invite_accepted.html.hbs delete mode 100644 mail/email/invite_confirmed.hbs delete mode 100644 mail/email/invite_confirmed.html.hbs delete mode 100644 mail/email/new_device_logged_in.hbs delete mode 100644 mail/email/new_device_logged_in.html.hbs delete mode 100644 mail/email/send_org_invite.hbs delete mode 100644 mail/email/send_org_invite.html.hbs delete mode 100644 mail/email/twofactor_email.hbs delete mode 100644 mail/email/twofactor_email.html.hbs delete mode 100644 mail/email/verify_email.hbs delete mode 100644 mail/email/verify_email.html.hbs delete mode 100644 mail/email/welcome.hbs delete mode 100644 mail/email/welcome.html.hbs create mode 100644 mail/maler.py 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.

-
- - -
- Sett hovedpassord -
-
- 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.

- -
- Bekreft adressen -
-

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'
      {punkter}
    ') + 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")