diff --git a/cryptify b/cryptify index 4c30e53..63066a1 160000 --- a/cryptify +++ b/cryptify @@ -1 +1 @@ -Subproject commit 4c30e539a04be1dc08cc1704de35f1ad4320c5af +Subproject commit 63066a1ea0f0a96fb684ff7f7b08537e27db8385 diff --git a/docker-compose.yml b/docker-compose.yml index 4f72948..391921b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,6 +18,12 @@ services: - ./cryptify/conf/config.dev.toml:/app/config.toml:ro - ./cryptify/src:/app/src - ./cryptify/templates:/app/templates + # build.rs feeds `PG_CORE_VERSION` (used by the X-PostGuard + # mail header) from Cargo.lock at compile time. Both files + # need to be visible inside the container, or cargo bails + # on `env!("PG_CORE_VERSION")`. + - ./cryptify/build.rs:/app/build.rs:ro + - ./cryptify/Cargo.lock:/app/Cargo.lock:ro - cryptify-target:/app/target environment: - RUST_LOG=info diff --git a/package-lock.json b/package-lock.json index 09bba54..aa909b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.7.0", "dependencies": { "@deltablot/dropzone": "^7.4.3", - "@e4a/pg-js": "^2.0.0", + "@e4a/pg-js": "^2.0.1", "@iconify/svelte": "^5.2.1", "@privacybydesign/yivi-css": "^1.0.1", "country-flag-icons": "^1.6.17", @@ -59,9 +59,9 @@ } }, "node_modules/@e4a/pg-js": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@e4a/pg-js/-/pg-js-2.0.0.tgz", - "integrity": "sha512-kPkaVP+wVnwtoHuugpYDEgXWvhNVjnMcAQRMleLC/8cSglG6T8UmSxeWps5E32/onHcqkxJNY2ygwijCjT/elg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@e4a/pg-js/-/pg-js-2.0.1.tgz", + "integrity": "sha512-bqlXc+RXMPAY71KFu4cuHXACD3RNAfZswuDYXWEvH8Lrphpf834BNTfoWGvrWJpebz4wbccAgAMMaMtSuzwSXA==", "license": "MIT", "dependencies": { "@e4a/pg-wasm": "^0.6.1", diff --git a/package.json b/package.json index b3636e0..ffaa19a 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ }, "dependencies": { "@deltablot/dropzone": "^7.4.3", - "@e4a/pg-js": "^2.0.0", + "@e4a/pg-js": "^2.0.1", "@iconify/svelte": "^5.2.1", "@privacybydesign/yivi-css": "^1.0.1", "country-flag-icons": "^1.6.17", diff --git a/postguard b/postguard index fff5796..cc47b60 160000 --- a/postguard +++ b/postguard @@ -1 +1 @@ -Subproject commit fff5796afb31ad0518a5396fdc362d83892f6e23 +Subproject commit cc47b608cbc9f636d6aa9fd07a79d161501f6a1f diff --git a/src/lib/components/filesharing/EmailPreviewModal.svelte b/src/lib/components/filesharing/EmailPreviewModal.svelte index 178ff30..58ef442 100644 --- a/src/lib/components/filesharing/EmailPreviewModal.svelte +++ b/src/lib/components/filesharing/EmailPreviewModal.svelte @@ -44,6 +44,10 @@ let data = $state(null) /** Index into the flat list of [...recipients, confirmation?] */ let activeIdx = $state(0) + /** Which MIME alternative to show in the body. Mail clients pick + * HTML when available; the plain-text branch is what text-only + * clients (or accessibility tools) actually render. */ + let bodyView: 'html' | 'text' = $state('html') let allEmails = $derived.by(() => { if (!data) return [] @@ -83,6 +87,24 @@ function handleKey(e: KeyboardEvent) { if (e.key === 'Escape') onClose() } + + /** Inject `` into the rendered email HTML so + * anchor clicks open in a new top-level tab instead of trying to + * navigate the sandboxed iframe (which has no `allow-top-navigation` + * and would otherwise blank the body). The `allow-popups` / + * `allow-popups-to-escape-sandbox` flags already permit the new + * tab. We try to splice into an existing ``; if cryptify ever + * changes the template shape, the prepended fallback still works + * because browsers tolerate a `` before ``. */ + function withBaseTarget(html: string): string { + const tag = '' + const headIdx = html.search(/]*>/i) + if (headIdx >= 0) { + const end = html.indexOf('>', headIdx) + 1 + return html.slice(0, end) + tag + html.slice(end) + } + return tag + html + } @@ -182,12 +204,37 @@ - +
+ + +
+ + {#if bodyView === 'html'} + + {:else} + + {/if} {/if} @@ -313,6 +360,39 @@ font-size: var(--pg-font-size-sm); } + .body-tabs { + display: flex; + gap: 0.25rem; + padding: 0.5rem 1.5rem 0; + background: var(--pg-soft-background); + border-bottom: 1px solid var(--pg-strong-background); + } + + .body-tab { + background: transparent; + border: 0; + border-bottom: 2px solid transparent; + padding: 0.4rem 0.8rem; + font-family: var(--pg-font-family); + font-size: var(--pg-font-size-sm); + color: var(--pg-text-secondary); + cursor: pointer; + } + + .body-tab:hover { + color: var(--pg-text); + } + + .body-tab.active { + color: var(--pg-text); + border-bottom-color: var(--pg-primary-contrast); + } + + .body-tab:focus-visible { + outline: 2px solid var(--pg-text); + outline-offset: 2px; + } + .email-frame { width: 100%; height: min(70vh, 720px); @@ -320,6 +400,20 @@ background: var(--pg-soft-background); } + .email-text { + margin: 0; + padding: 1rem 1.5rem; + max-height: min(70vh, 720px); + overflow: auto; + background: var(--pg-soft-background); + color: var(--pg-text); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: var(--pg-font-size-sm); + line-height: 1.5; + white-space: pre-wrap; + word-break: break-word; + } + .state { padding: 2rem 1.5rem; text-align: center; diff --git a/src/lib/components/filesharing/inputs/FileInput.svelte b/src/lib/components/filesharing/inputs/FileInput.svelte index 33098ed..7fce24d 100644 --- a/src/lib/components/filesharing/inputs/FileInput.svelte +++ b/src/lib/components/filesharing/inputs/FileInput.svelte @@ -87,6 +87,17 @@ }) myDropzone.on('addedfile', (file) => { + const isDuplicate = files.some( + (f) => + f.name === file.name && + f.size === file.size && + f.lastModified === file.lastModified + ) + if (isDuplicate) { + myDropzone!.removeFile(file) + return + } + files = files.concat([file]) percentages = percentages.concat([0]) done = done.concat([false]) diff --git a/src/lib/locales/en.json b/src/lib/locales/en.json index f33920d..cd9e90e 100644 --- a/src/lib/locales/en.json +++ b/src/lib/locales/en.json @@ -229,21 +229,6 @@ "addRecipient": "recipient", "addRecipientButton": "Add another recipient", "emailSenderConfirm": "Send me a confirmation", - "emailPreview": { - "title": "Notification email preview", - "subtitle": "Staging only — cryptify does not actually send notification emails on staging. This is what each recipient would have received.", - "loading": "Loading preview…", - "error": "Could not load preview.", - "empty": "No recipients to preview.", - "close": "Close", - "from": "From", - "replyTo": "Reply to", - "subject": "Subject", - "to": "To", - "confirmationTag": "sender copy", - "iframeTitle": "Notification email body", - "reopen": "Show email preview" - }, "timeremaining": { "estimate": "Estimating...", "unknown": "More then one day left.", @@ -271,6 +256,23 @@ "serverBlocked": "The server rejected this upload because you have used {used} GB of your {limit} GB 2-week limit. Your limit resets on {resets}." } }, + "emailPreview": { + "title": "Notification email preview", + "subtitle": "Staging only — cryptify does not actually send notification emails on staging. This is what each recipient would have received.", + "loading": "Loading preview…", + "error": "Could not load preview.", + "empty": "No recipients to preview.", + "close": "Close", + "from": "From", + "replyTo": "Reply to", + "subject": "Subject", + "to": "To", + "confirmationTag": "sender copy", + "iframeTitle": "Notification email body", + "reopen": "Show email preview", + "viewHtml": "HTML", + "viewText": "Plain text" + }, "attributes": { "pbdf.sidn-pbdf.mobilenumber.mobilenumber": "Mobile phone number", "pbdf.sidn-pbdf.mobilenumber.mobilenumber.placeholder": "612345678", diff --git a/src/lib/locales/nl.json b/src/lib/locales/nl.json index da80cfe..9a585f0 100644 --- a/src/lib/locales/nl.json +++ b/src/lib/locales/nl.json @@ -228,21 +228,6 @@ "addRecipient": "ontvanger", "addRecipientButton": "Voeg nog een ontvanger toe", "emailSenderConfirm": "Stuur mij een bevestiging", - "emailPreview": { - "title": "Voorbeeld notificatie-e-mail", - "subtitle": "Alleen staging — cryptify verstuurt op staging geen e-mails. Dit is wat elke ontvanger zou hebben gezien.", - "loading": "Voorbeeld laden…", - "error": "Voorbeeld kon niet worden geladen.", - "empty": "Geen ontvangers om weer te geven.", - "close": "Sluiten", - "from": "Van", - "replyTo": "Antwoord aan", - "subject": "Onderwerp", - "to": "Aan", - "confirmationTag": "bevestigingskopie", - "iframeTitle": "Inhoud van de notificatie-e-mail", - "reopen": "Toon e-mailvoorbeeld" - }, "timeremaining": { "estimate": "Estimating...", "unknown": "Nog meer dan een dag", @@ -270,6 +255,23 @@ "serverBlocked": "De server weigerde deze upload omdat je {used} GB van je {limit} GB-limiet (2 weken) hebt gebruikt. De limiet reset op {resets}." } }, + "emailPreview": { + "title": "Voorbeeld notificatie-e-mail", + "subtitle": "Alleen staging — cryptify verstuurt op staging geen e-mails. Dit is wat elke ontvanger zou hebben gezien.", + "loading": "Voorbeeld laden…", + "error": "Voorbeeld kon niet worden geladen.", + "empty": "Geen ontvangers om weer te geven.", + "close": "Sluiten", + "from": "Van", + "replyTo": "Antwoord aan", + "subject": "Onderwerp", + "to": "Aan", + "confirmationTag": "bevestigingskopie", + "iframeTitle": "Inhoud van de notificatie-e-mail", + "reopen": "Toon e-mailvoorbeeld", + "viewHtml": "HTML", + "viewText": "Platte tekst" + }, "attributes": { "pbdf.sidn-pbdf.mobilenumber.mobilenumber": "Mobiel telefoonnummer", "pbdf.sidn-pbdf.mobilenumber.mobilenumber.placeholder": "612345678",