diff --git a/docs/repos/postguard-js.md b/docs/repos/postguard-js.md index 263d233..bc45d00 100644 --- a/docs/repos/postguard-js.md +++ b/docs/repos/postguard-js.md @@ -205,7 +205,9 @@ const envelope = await pg.email.createEnvelope({ sealed, from: 'alice@example.co // envelope.subject → "PostGuard Encrypted Email" // envelope.htmlBody → Placeholder HTML with PostGuard branding // envelope.plainTextBody → Plain text fallback -// envelope.attachment → File("postguard.encrypted") +// envelope.attachment → File("postguard.encrypted") | null (null in tier 3, see /sdk/js-email-helpers) +// envelope.tier → "tier1" | "tier2" | "tier3" +// envelope.uploadUuid → Cryptify UUID (string) when uploaded, otherwise null // --- On the receiving side --- const ciphertext = extractCiphertext({ diff --git a/docs/repos/postguard-tb-addon.md b/docs/repos/postguard-tb-addon.md index 3ad3f93..7f65b6c 100644 --- a/docs/repos/postguard-tb-addon.md +++ b/docs/repos/postguard-tb-addon.md @@ -177,6 +177,10 @@ async function handleEncrypt(pg: PostGuard, data: EncryptPopupData, windowId: nu [Source: yivi-popup.ts#L90-L136](https://github.com/encryption4all/postguard-tb-addon/blob/57234eebd32d64bd011086fe89ecdd7ac40fc15d/src/pages/yivi-popup/yivi-popup.ts#L90-L136) +::: warning Tier 3 envelopes have no attachment +As of `@e4a/pg-js` 0.10, `envelope.attachment` is `File | null` and is `null` for tier 3 envelopes (ciphertext over `PG_MAX_ATTACHMENT_SIZE`, ~10 MB by default). The snippet above dereferences `envelope.attachment.arrayBuffer()` directly, so it works only when the payload falls into tier 1 or tier 2. The addon needs a null branch that skips the `attachmentBase64` field and relies on the Cryptify download link in `envelope.htmlBody` instead. Tracked separately. See [Email Helpers](/sdk/js-email-helpers#createenvelope) for the tier model. +::: + ### Decrypt Handler ```ts diff --git a/docs/sdk/js-email-helpers.md b/docs/sdk/js-email-helpers.md index 8a4f19c..1194127 100644 --- a/docs/sdk/js-email-helpers.md +++ b/docs/sdk/js-email-helpers.md @@ -74,11 +74,48 @@ Provide at least one of `htmlBody` or `plainTextBody`. If both are provided, the ## `createEnvelope()` -Takes a `Sealed` encryption builder and wraps the encrypted output into an email envelope. The function is async because it encrypts the data and may upload to Cryptify if the payload is too large. +Takes a `Sealed` encryption builder and wraps the encrypted output into an email envelope. The function is async because it encrypts the data and may upload to Cryptify. -The envelope contains a placeholder HTML body (telling the recipient to use PostGuard to decrypt), a plain text fallback, and the ciphertext as a file attachment named `postguard.encrypted`. +The envelope contains a placeholder HTML body (telling the recipient to use PostGuard to decrypt), a plain text fallback, and (in most cases) the ciphertext as a file attachment named `postguard.encrypted`. -For small payloads (under 100 KB), the encrypted data is also embedded as an armored base64 block in the HTML and as a URL fragment in the decrypt button link. For large payloads, `createEnvelope` automatically uploads to Cryptify and puts a download link in the email instead. +### Tier model + +`createEnvelope` picks one of three tiers based on the encrypted payload size. Each tier decides whether to attach the ciphertext locally, whether to upload it to Cryptify, and which kind of fallback link to put in the body. + +| Tier | Selected when | Local attachment | Cryptify upload | Body fallback link | +|------|---------------|------------------|-----------------|--------------------| +| 1 | base64 ciphertext length ≤ `PG_MAX_URL_FRAGMENT_SIZE` | yes | no | `/decrypt#` (whole ciphertext in the URL fragment) | +| 2 | ciphertext bytes ≤ `PG_MAX_ATTACHMENT_SIZE` | yes | yes (opt out with `uploadToCryptify: false`) | `/decrypt?uuid=…` (data) or `/download?uuid=…` (files) | +| 3 | ciphertext bytes > `PG_MAX_ATTACHMENT_SIZE` | no | yes (always) | `/decrypt?uuid=…` (data) or `/download?uuid=…` (files) | + +Tier 3 omits the local attachment because Exchange tenants typically reject messages with attachments above ~25 MB. Recipients of a tier 3 envelope rely on the Cryptify download link in the body. + +The constants are exported from `@e4a/pg-js`: + +| Constant | Default | Meaning | +|----------|---------|---------| +| `PG_MAX_URL_FRAGMENT_SIZE` | `100_000` | Tier 1 cap, in characters of base64 ciphertext | +| `PG_MAX_ATTACHMENT_SIZE` | `10 * 1024 * 1024` | Tier 2/3 boundary, in bytes of binary ciphertext | + +[Source: extract.ts#L1-L12](https://github.com/encryption4all/postguard-js/blob/91c84855b4613e9c8c1fe65fc0f5a4dc4c6d11d6/src/email/extract.ts#L1-L12) + +The tier-selection logic itself is a few lines: + +```ts +function pickTier(encryptedBytes: number, base64Length: number): EnvelopeTier { + if (base64Length <= PG_MAX_URL_FRAGMENT_SIZE) return 'tier1'; + if (encryptedBytes <= PG_MAX_ATTACHMENT_SIZE) return 'tier2'; + return 'tier3'; +} +``` + +[Source: envelope.ts#L141-L145](https://github.com/encryption4all/postguard-js/blob/91c84855b4613e9c8c1fe65fc0f5a4dc4c6d11d6/src/email/envelope.ts#L141-L145) + +::: warning Breaking change in 0.10 +Earlier releases always emitted a hidden `
` block in `htmlBody` carrying the full base64 ciphertext, and `attachment` was always a `File`. Both have changed. The armor block has been removed (it pushed bodies past Outlook's 1 M-character `setAsync` limit), and `attachment` is now `File | null` — null for tier 3. +::: + +### Usage The Thunderbird addon creates the envelope in one call: @@ -101,6 +138,8 @@ const envelope = await pg.email.createEnvelope({ [Source: yivi-popup.ts#L90-L136](https://github.com/encryption4all/postguard-tb-addon/blob/57234eebd32d64bd011086fe89ecdd7ac40fc15d/src/pages/yivi-popup/yivi-popup.ts#L90-L136) +Callers that handle the result must null-check `envelope.attachment` before reading it, since tier 3 envelopes carry no attachment. + ### Parameters | Parameter | Type | Required | Description | @@ -109,24 +148,23 @@ const envelope = await pg.email.createEnvelope({ | `from` | `string` | Yes | Sender email address | | `websiteUrl` | `string` | No | URL to link in the placeholder body (default: `https://postguard.eu`) | | `unencryptedMessage` | `string` | No | Unencrypted message shown in the placeholder | +| `senderAttributes` | `string[]` | No | Verified sender attributes to display below the sender name | +| `uploadToCryptify` | `boolean` | No | Default `true`. Set `false` to keep tier 2 envelopes as a local attachment only and skip the Cryptify upload + body link. Has no effect on tier 1 (no upload happens) or tier 3 (upload is always attempted because there is no fallback). | ### Result | Property | Type | Description | |----------|------|-------------| | `subject` | `string` | Always `"PostGuard Encrypted Email"` | -| `htmlBody` | `string` | Placeholder HTML with decrypt button and armored payload | +| `htmlBody` | `string` | Placeholder HTML with the decrypt button and the fallback link for the selected tier | | `plainTextBody` | `string` | Plain text fallback | -| `attachment` | `File` | The `postguard.encrypted` file | +| `attachment` | `File \| null` | The `postguard.encrypted` file in tiers 1 and 2, `null` in tier 3 | +| `tier` | `'tier1' \| 'tier2' \| 'tier3'` | Which tier was selected | +| `uploadUuid` | `string \| null` | Cryptify UUID if the payload was uploaded, otherwise `null` | ## `extractCiphertext()` -Extracts the encrypted ciphertext from a received email. It checks two locations in order: - -1. Attachments: looks for a file named `postguard.encrypted` -2. HTML body: looks for an armored payload between `-----BEGIN POSTGUARD MESSAGE-----` and `-----END POSTGUARD MESSAGE-----` markers - -Returns a `Uint8Array` with the ciphertext, or `null` if nothing is found. +Extracts the encrypted ciphertext from a received email by looking for an attachment named `postguard.encrypted`. Returns a `Uint8Array` with the ciphertext, or `null` if no such attachment is found. ```ts const ciphertext = pg.email.extractCiphertext({ @@ -135,13 +173,29 @@ const ciphertext = pg.email.extractCiphertext({ }); ``` +[Source: extract.ts#L14-L28](https://github.com/encryption4all/postguard-js/blob/91c84855b4613e9c8c1fe65fc0f5a4dc4c6d11d6/src/email/extract.ts#L14-L28) + +Tier 3 envelopes carry no attachment, so `extractCiphertext` returns `null` on them. Pair it with `extractUploadUuid` to find a Cryptify UUID in the body and download the ciphertext from there. + +The `htmlBody` field is accepted for compatibility but is no longer consulted. The legacy in-body armor block (`
` and the `-----BEGIN POSTGUARD MESSAGE-----` markers) is no longer emitted, and consumer code that stripped or parsed it can be removed. + ### Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| -| `htmlBody` | `string` | No | The HTML body of the received email | +| `htmlBody` | `string` | No | Accepted for compatibility, no longer consulted | | `attachments` | `Array<{ name, data }>` | No | Email attachments (data as ArrayBuffer) | +## `extractUploadUuid()` + +Finds a Cryptify UUID in the HTML body of a received email. It matches either the `/decrypt?uuid=…` or `/download?uuid=…` link produced by tier 2 and tier 3 envelopes. Returns the UUID, or `null` if none is found. + +```ts +const uuid = pg.email.extractUploadUuid(htmlBody); +``` + +[Source: extract.ts#L35-L43](https://github.com/encryption4all/postguard-js/blob/91c84855b4613e9c8c1fe65fc0f5a4dc4c6d11d6/src/email/extract.ts#L35-L43) + ## `injectMimeHeaders()` Adds or replaces headers in a raw MIME string. The function splits the MIME at the `\r\n\r\n` separator, processes the header section (including folded multi-line headers), and reassembles the result.