diff --git a/src/lib/crypto/__tests__/compose-secure.test.ts b/src/lib/crypto/__tests__/compose-secure.test.ts index dac3936a..ac73ad25 100644 --- a/src/lib/crypto/__tests__/compose-secure.test.ts +++ b/src/lib/crypto/__tests__/compose-secure.test.ts @@ -102,6 +102,18 @@ describe('prepareSecureSend', () => { expect(new TextDecoder().decode(uploaded!)).not.toMatch(/bcc@example.org/i); }, 60_000); + it('quotes a Reply-To name with a comma so it stays one address', async () => { + await importPgpKey(me.privateKey, me.passphrase); + let uploaded: Uint8Array | null = null; + const msg = { ...outgoing(), replyTo: [{ name: 'Support, ACME', email: 'help@acme.com' }] }; + await prepareSecureSend(msg, { mode: 'pgp', sign: true, encrypt: false }, { + fetchBlob: async () => new Uint8Array([1]), + upload: async (bytes) => { uploaded = bytes; return 'x'; }, + }); + const text = new TextDecoder().decode(uploaded!); + expect(text).toMatch(/^Reply-To: "Support, ACME" $/m); + }, 60_000); + it('stops when a recipient has no key, or when the signing key is locked', async () => { await importPgpKey(me.privateKey, me.passphrase); const deps = { fetchBlob: async () => new Uint8Array(), upload: async () => 'x' }; diff --git a/src/lib/crypto/compose-secure.ts b/src/lib/crypto/compose-secure.ts index 053f7b30..5135b5e9 100644 --- a/src/lib/crypto/compose-secure.ts +++ b/src/lib/crypto/compose-secure.ts @@ -8,7 +8,7 @@ import type { OutgoingEmail } from '../../api/email'; import type { ContactCard } from '../../api/types'; import { useCryptoKeysStore, keysForEmail, isRecordCurrent, type CryptoKeyRecord, type PgpKeyRecord, type SmimeCertRecord } from '../../stores/crypto-keys-store'; import { messagePriorityHeaders } from '../message-priority'; -import { base64Decode, formatHeader, encodeHeaderWords, type MimeAttachment, type MimeMessageInput } from './mime'; +import { base64Decode, formatHeader, formatAddress, type MimeAttachment, type MimeMessageInput } from './mime'; import { buildSecureMessage, type SecureMode } from './secure-send'; import { ownKeyFor, unlockedPgpKey, unlockedSmimeKey } from './keyring'; @@ -100,7 +100,7 @@ export function secureReadiness(choice: SecureChoice, fromEmail: string, recipie function extraHeaders(outgoing: OutgoingEmail): string[] { const lines: string[] = []; if (outgoing.replyTo?.length) { - lines.push(formatHeader('Reply-To', outgoing.replyTo.map((a) => (a.name ? `${encodeHeaderWords(a.name)} <${a.email}>` : a.email)).join(', '))); + lines.push(formatHeader('Reply-To', outgoing.replyTo.map(formatAddress).join(', '))); } for (const [prop, value] of Object.entries(messagePriorityHeaders(outgoing.priority))) { const name = /^header:([^:]+):/.exec(prop)?.[1]; diff --git a/src/lib/crypto/mime.ts b/src/lib/crypto/mime.ts index e6df1a96..71f8da03 100644 --- a/src/lib/crypto/mime.ts +++ b/src/lib/crypto/mime.ts @@ -90,7 +90,8 @@ function unfold(value: string | undefined): string { return String(value ?? '').replace(/[\r\n\t]+/g, ' ').replace(/ {2,}/g, ' ').trim(); } -function formatAddress(addr: MimeAddress): string { +/** One address for a header: the name quoted (or RFC 2047-encoded) so commas in it stay inside. */ +export function formatAddress(addr: MimeAddress): string { if (addr.name) { const name = unfold(addr.name); // Raw 8-bit in a header is rejected by some MTAs; encode it.