Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
106 changes: 100 additions & 6 deletions src/lib/components/filesharing/EmailPreviewModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@
let data = $state<PreviewResponse | null>(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<RenderedEmail[]>(() => {
if (!data) return []
Expand Down Expand Up @@ -83,6 +87,24 @@
function handleKey(e: KeyboardEvent) {
if (e.key === 'Escape') onClose()
}

/** Inject `<base target="_blank">` 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 `<head>`; if cryptify ever
* changes the template shape, the prepended fallback still works
* because browsers tolerate a `<base>` before `<html>`. */
function withBaseTarget(html: string): string {
const tag = '<base target="_blank" rel="noopener">'
const headIdx = html.search(/<head[^>]*>/i)
if (headIdx >= 0) {
const end = html.indexOf('>', headIdx) + 1
return html.slice(0, end) + tag + html.slice(end)
}
return tag + html
}
</script>

<svelte:window onkeydown={handleKey} />
Expand Down Expand Up @@ -182,12 +204,37 @@
</div>
</section>

<iframe
class="email-frame"
title={$_('filesharing.emailPreview.iframeTitle')}
srcdoc={active.html}
sandbox="allow-popups allow-popups-to-escape-sandbox"
></iframe>
<div class="body-tabs" role="tablist">
<button
type="button"
role="tab"
aria-selected={bodyView === 'html'}
class="body-tab"
class:active={bodyView === 'html'}
onclick={() => (bodyView = 'html')}
>{$_('filesharing.emailPreview.viewHtml')}</button
>
<button
type="button"
role="tab"
aria-selected={bodyView === 'text'}
class="body-tab"
class:active={bodyView === 'text'}
onclick={() => (bodyView = 'text')}
>{$_('filesharing.emailPreview.viewText')}</button
>
</div>

{#if bodyView === 'html'}
<iframe
class="email-frame"
title={$_('filesharing.emailPreview.iframeTitle')}
srcdoc={withBaseTarget(active.html)}
sandbox="allow-popups allow-popups-to-escape-sandbox"
></iframe>
{:else}
<pre class="email-text">{active.text}</pre>
{/if}
{/if}
</div>
</div>
Expand Down Expand Up @@ -313,13 +360,60 @@
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);
border: 0;
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;
Expand Down
11 changes: 11 additions & 0 deletions src/lib/components/filesharing/inputs/FileInput.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down
32 changes: 17 additions & 15 deletions src/lib/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down Expand Up @@ -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",
Expand Down
32 changes: 17 additions & 15 deletions src/lib/locales/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down