Files
vaultnord-brand/mail/maler.py
T
2026-08-31 21:46:28 +00:00

618 lines
28 KiB
Python

"""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 <utmappe>
og legger igjen <utmappe>/<språk>/email/<navn>.hbs og <navn>.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 &middot; drift i Finland, backup i Tyskland &mdash; alt innenfor EU.",
"en": "Vaultnord &middot; hosted in Finland, backed up in Germany &mdash; 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'<li style="margin:0 0 4px">{_inline(linje[2:])}</li>'
for linje in a.split("\n"))
ut.append(f'<ul style="{p};padding-left:20px">{punkter}</ul>')
else:
ut.append(f'<p style="{p}">{_inline(a)}</p>')
return "\n ".join(ut)
def _inline(s: str) -> str:
s = LENKE.sub(r'<a href="\2" style="color:#17807d">\1</a>', s)
return UTHEVET.sub(r"<strong>\1</strong>", s)
RAMME = """<!doctype html>
<html lang="{lang}"><body style="margin:0;padding:0;background:#f3f6fa">
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="background:#f3f6fa">
<tr><td align="center" style="padding:32px 16px">
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="max-width:560px;background:#ffffff;border:1px solid #dbe2ec;border-radius:12px;font-family:-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif">
<tr><td style="padding:28px 32px 4px;font-size:20px;font-weight:700;letter-spacing:-.02em;color:#2e3440">vaultnord</td></tr>
<tr><td style="padding:12px 32px 0">
<h1 style="margin:0 0 18px;font-size:20px;line-height:1.3;color:#2e3440;font-weight:650">{tittel}</h1>
{kropp}
</td></tr>
<tr><td style="padding:18px 32px 26px;border-top:1px solid #dbe2ec;color:#55617a;font-size:13px;line-height:1.6">
{bunn}
</td></tr>
</table>
</td></tr>
</table>
</body></html>"""
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)}
mangler = _mangler(finnes)
if mangler:
raise SystemExit("maler.py: serveren har maler vi ikke har skrevet: "
+ ", ".join(sorted(mangler)))
# Partialene har ingen egen tekst; de innebygde malene bruker dem, våre ikke.
PARTIALER = {"email_header", "email_footer", "email_footer_text"}
def _mangler(finnes: set[str]) -> set[str]:
"""Hvilke av serverens maler vi ikke har skrevet.
Navnene plukkes ut av en binærfil, så treffene inneholder avkuttede biter -
«sm» der det står smtp_test, «we» der det står welcome. Et navn som bare er
starten på et annet navn er et slikt fragment, ikke en mal.
Prisen er at en oppstrøms mal som heter nøyaktig starten på en vi allerede
har, ville sluppet gjennom. Det har ikke skjedd, og alternativet - å la
fragmentene stoppe hvert eneste bygg - er verre."""
kjente = finnes | set(MALER)
ekte = {n for n in finnes - PARTIALER
if not any(k != n and k.startswith(n) for k in kjente)}
return ekte - set(MALER)
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")