Skip to content

Email Helpers

The SDK includes email helper methods for building and parsing PostGuard-encrypted emails. They are available both as instance methods on pg.email.* and as standalone imports from @e4a/pg-js. The standalone imports are useful in contexts like extension background scripts where you don't need a full PostGuard instance. All examples below come from the Thunderbird addon.

ts
// Standalone imports (no PostGuard instance needed)
import { buildMime, extractCiphertext, injectMimeHeaders, createEnvelope } from '@e4a/pg-js';

// Or use instance methods
pg.email.buildMime(...)
pg.email.extractCiphertext(...)

Overview

Encrypting an email with PostGuard follows this workflow:

1. Build inner MIME    -->  buildMime()  or  pg.email.buildMime()
2. Encrypt the MIME    -->  pg.encrypt({ data: mimeBytes })
3. Create envelope     -->  pg.email.createEnvelope({ sealed, from })
4. Send the envelope via your email client / API

Decrypting reverses the process:

1. Extract ciphertext  -->  extractCiphertext()  or  pg.email.extractCiphertext()
2. Open + decrypt      -->  pg.open({ data }).decrypt({ session })
3. Parse the plaintext MIME

buildMime()

Constructs a MIME message from structured input. Returns the raw MIME bytes as a Uint8Array. The output includes proper headers (Date, MIME-Version, Content-Type, X-PostGuard) and handles multipart encoding for attachments.

The Thunderbird addon builds the inner MIME from compose details:

ts
const mimeData = buildMime({
  from: details.from,
  to: [...details.to],
  cc: [...details.cc],
  subject: originalSubject,
  htmlBody: details.isPlainText ? undefined : details.body,
  plainTextBody: details.isPlainText ? details.plainTextBody : undefined,
  date,
  inReplyTo,
  references,
  attachments: attachmentData,
});

Source: background.ts#L331-L342

Parameters

ParameterTypeRequiredDescription
fromstringYesSender email address
tostring[]YesRecipient email addresses
ccstring[]NoCC email addresses
subjectstringYesEmail subject line
htmlBodystringNoHTML body content
plainTextBodystringNoPlain text body content
dateDateNoSend date (defaults to now)
inReplyTostringNoMessage-ID of the email being replied to
referencesstringNoReferences header for threading
attachmentsArray<{ name, type, data }>NoFile attachments (data as ArrayBuffer)

TIP

Provide at least one of htmlBody or plainTextBody. If both are provided, the MIME message includes both as a multipart/alternative section. If attachments are present, the message uses multipart/mixed.

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.

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.

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.

TierSelected whenLocal attachmentCryptify uploadBody fallback link
1base64 ciphertext length ≤ PG_MAX_URL_FRAGMENT_SIZEyesno/decrypt#<base64> (whole ciphertext in the URL fragment)
2ciphertext bytes ≤ PG_MAX_ATTACHMENT_SIZEyesyes (opt out with uploadToCryptify: false)/decrypt?uuid=… (data) or /download?uuid=… (files)
3ciphertext bytes > PG_MAX_ATTACHMENT_SIZEnoyes (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:

ConstantDefaultMeaning
PG_MAX_URL_FRAGMENT_SIZE100_000Tier 1 cap, in characters of base64 ciphertext
PG_MAX_ATTACHMENT_SIZE10 * 1024 * 1024Tier 2/3 boundary, in bytes of binary ciphertext

Source: 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

Breaking change in 0.10

Earlier releases always emitted a hidden <div id="postguard-armor"> 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.

An optional &recipient=<key> may ride alongside either query-string form. When present and matching one of the policy recipients, the recipient page skips the picker and authenticates against that key directly. The matching parser logic on the recipient side lives in postguard-website.

Usage

The Thunderbird addon creates the envelope in one call:

ts
const sealed = pg.encrypt({
  sign: pg.sign.yivi({
    element: "#yivi-web-form",
    senderEmail: data.senderEmail,
  }),
  recipients,
  data: mimeData,
});

const envelope = await pg.email.createEnvelope({
  sealed,
  from: data.from,
  websiteUrl: data.websiteUrl,
});

Source: 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

ParameterTypeRequiredDescription
sealedSealedYesThe Sealed encryption builder from pg.encrypt()
fromstringYesSender email address
websiteUrlstringNoURL to link in the placeholder body (default: https://postguard.eu)
unencryptedMessagestringNoUnencrypted message shown in the placeholder
senderAttributesstring[]NoVerified sender attributes to display below the sender name
uploadToCryptifybooleanNoDefault 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

PropertyTypeDescription
subjectstringAlways "PostGuard Encrypted Email"
htmlBodystringPlaceholder HTML with the decrypt button and the fallback link for the selected tier
plainTextBodystringPlain text fallback
attachmentFile | nullThe postguard.encrypted file in tiers 1 and 2, null in tier 3
tier'tier1' | 'tier2' | 'tier3'Which tier was selected
uploadUuidstring | nullCryptify UUID if the payload was uploaded, otherwise null

extractCiphertext()

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({
  htmlBody: bodyHtml,
  attachments: attachmentData,
});

Source: 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 (<div id="postguard-armor"> and the -----BEGIN POSTGUARD MESSAGE----- markers) is no longer emitted, and consumer code that stripped or parsed it can be removed.

Parameters

ParameterTypeRequiredDescription
htmlBodystringNoAccepted for compatibility, no longer consulted
attachmentsArray<{ name, data }>NoEmail 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

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.

The Thunderbird addon injects threading headers and an X-PostGuard marker into decrypted messages:

ts
// Inject threading headers from the outer (encrypted) message
let raw = pg.email.injectMimeHeaders(
  plaintext,
  { "In-Reply-To": inReplyTo, "References": references },
);

// Mark it as a PostGuard-decrypted message
raw = pg.email.injectMimeHeaders(raw, { "X-PostGuard": "decrypted" });

Parameters

ParameterTypeRequiredDescription
mimestringYesThe raw MIME string
headersToInjectRecord<string, string>YesHeaders to add
headersToRemovestring[]NoHeaders to remove first