@aglyn/plugins-outreach 1.0.0-beta.213 → 1.0.0-beta.215
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -14
- package/src/lib/components/enrollment-detail.js +13 -3
- package/src/lib/components/enrollment-detail.js.map +1 -1
- package/src/lib/components/sequence-editor.js +23 -1
- package/src/lib/components/sequence-editor.js.map +1 -1
- package/src/lib/components/sequence-report-card.d.ts +3 -2
- package/src/lib/components/sequence-report-card.js +29 -1
- package/src/lib/components/sequence-report-card.js.map +1 -1
- package/src/lib/engine/click-tracking.d.ts +9 -0
- package/src/lib/engine/click-tracking.js +2 -2
- package/src/lib/engine/click-tracking.js.map +1 -1
- package/src/lib/engine/compose.d.ts +13 -0
- package/src/lib/engine/compose.js +4 -0
- package/src/lib/engine/compose.js.map +1 -1
- package/src/lib/engine/open-tracking.d.ts +61 -0
- package/src/lib/engine/open-tracking.js +164 -0
- package/src/lib/engine/open-tracking.js.map +1 -0
- package/src/lib/engine/sequence-validation.js +3 -0
- package/src/lib/engine/sequence-validation.js.map +1 -1
- package/src/lib/model/enrollment-engagement.d.ts +6 -0
- package/src/lib/model/enrollment-engagement.js +17 -1
- package/src/lib/model/enrollment-engagement.js.map +1 -1
- package/src/lib/model/enrollment-history.d.ts +8 -0
- package/src/lib/model/enrollment-history.js +29 -0
- package/src/lib/model/enrollment-history.js.map +1 -1
- package/src/lib/model/enrollment-timeline.d.ts +7 -1
- package/src/lib/model/enrollment-timeline.js +41 -4
- package/src/lib/model/enrollment-timeline.js.map +1 -1
- package/src/lib/model/outreach.types.d.ts +71 -4
- package/src/lib/model/outreach.types.js +3 -2
- package/src/lib/model/outreach.types.js.map +1 -1
- package/src/lib/model/sequence-draft.js +3 -0
- package/src/lib/model/sequence-draft.js.map +1 -1
- package/src/lib/model/sequence-report.d.ts +25 -2
- package/src/lib/model/sequence-report.js +36 -10
- package/src/lib/model/sequence-report.js.map +1 -1
- package/src/lib/model/step-preview.d.ts +6 -0
- package/src/lib/model/step-preview.js +3 -2
- package/src/lib/model/step-preview.js.map +1 -1
- package/src/lib/model/stored-records.js +9 -1
- package/src/lib/model/stored-records.js.map +1 -1
- package/src/lib/routes/step-test-routes.js +40 -12
- package/src/lib/routes/step-test-routes.js.map +1 -1
- package/src/lib/runtime/click-events.d.ts +2 -0
- package/src/lib/runtime/click-events.js +6 -6
- package/src/lib/runtime/click-events.js.map +1 -1
- package/src/lib/runtime/click-link.d.ts +25 -0
- package/src/lib/runtime/click-link.js +49 -0
- package/src/lib/runtime/click-link.js.map +1 -1
- package/src/lib/runtime/click-route.d.ts +4 -0
- package/src/lib/runtime/click-route.js +46 -3
- package/src/lib/runtime/click-route.js.map +1 -1
- package/src/lib/runtime/open-events.d.ts +55 -0
- package/src/lib/runtime/open-events.js +121 -0
- package/src/lib/runtime/open-events.js.map +1 -0
- package/src/lib/runtime/send-job.js +44 -8
- package/src/lib/runtime/send-job.js.map +1 -1
- package/src/lib/transport/rfc5322.d.ts +14 -3
- package/src/lib/transport/rfc5322.js +44 -9
- package/src/lib/transport/rfc5322.js.map +1 -1
- package/src/lib/transport/send-message.js +2 -1
- package/src/lib/transport/send-message.js.map +1 -1
|
@@ -292,7 +292,7 @@ const isAscii = (value)=>isPrintableAscii(value);
|
|
|
292
292
|
return body.split(CRLF).every((line)=>isAscii(line) && line.length <= MAX_LINE);
|
|
293
293
|
}
|
|
294
294
|
/** Builds the message. Throws {@link Rfc5322MessageError} on anything unsafe. */ export function buildRfc5322Message(message, options = {}) {
|
|
295
|
-
var _options_messageIdDomain, _options_messageIdLocalPart, _options_date;
|
|
295
|
+
var _options_messageIdDomain, _options_messageIdLocalPart, _options_date, _options_boundary;
|
|
296
296
|
const from = normalizeAddress(message.from, 'From');
|
|
297
297
|
const to = normalizeAddress(message.to, 'To');
|
|
298
298
|
const subject = subjectTokens(message.subject);
|
|
@@ -303,9 +303,7 @@ const isAscii = (value)=>isPrintableAscii(value);
|
|
|
303
303
|
throw new Rfc5322MessageError('invalid-message-id', 'The Message-ID would not be valid.');
|
|
304
304
|
}
|
|
305
305
|
const messageId = `<${local}@${domain}>`;
|
|
306
|
-
const
|
|
307
|
-
const sevenBit = isSevenBitSafe(body);
|
|
308
|
-
const lines = [
|
|
306
|
+
const headers = [
|
|
309
307
|
foldHeader('From', formatAddress(from)),
|
|
310
308
|
foldHeader('To', formatAddress(to)),
|
|
311
309
|
...subject.length ? [
|
|
@@ -316,17 +314,54 @@ const isAscii = (value)=>isPrintableAscii(value);
|
|
|
316
314
|
`Date: ${formatRfc5322Date((_options_date = options.date) != null ? _options_date : new Date())}`,
|
|
317
315
|
`Message-ID: ${messageId}`,
|
|
318
316
|
...extras.map(([name, value])=>foldHeader(name, spaceTokens(value))),
|
|
319
|
-
'MIME-Version: 1.0'
|
|
320
|
-
'Content-Type: text/plain; charset=UTF-8',
|
|
321
|
-
`Content-Transfer-Encoding: ${sevenBit ? '7bit' : 'quoted-printable'}`
|
|
317
|
+
'MIME-Version: 1.0'
|
|
322
318
|
];
|
|
323
|
-
const
|
|
319
|
+
const text = mimePart('text/plain', message.text);
|
|
320
|
+
const html = typeof message.html === 'string' && message.html.trim() ? mimePart('text/html', message.html) : null;
|
|
321
|
+
if (!html) {
|
|
322
|
+
return {
|
|
323
|
+
raw: `${[
|
|
324
|
+
...headers,
|
|
325
|
+
...text.headers
|
|
326
|
+
].join(CRLF)}${CRLF}${CRLF}${text.body}`,
|
|
327
|
+
messageId,
|
|
328
|
+
subject: message.subject.trim()
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
/*
|
|
332
|
+
* RFC 2046 §5.1.4: alternatives in increasing order of preference, so the
|
|
333
|
+
* plain text comes first and a client that can show HTML shows the HTML.
|
|
334
|
+
* The boundary is a UUID's characters behind a prefix, which no
|
|
335
|
+
* quoted-printable or 7bit body line can contain in full.
|
|
336
|
+
*/ const boundary = (_options_boundary = options.boundary) != null ? _options_boundary : `aglyn-alt-${randomUUID()}`;
|
|
337
|
+
if (!/^[A-Za-z0-9'()+_,./:=?-]{1,70}$/.test(boundary)) {
|
|
338
|
+
throw new Rfc5322MessageError('invalid-header', 'The multipart boundary would not be valid.');
|
|
339
|
+
}
|
|
340
|
+
const part = (built)=>`--${boundary}${CRLF}${built.headers.join(CRLF)}${CRLF}${CRLF}${built.body}`;
|
|
324
341
|
return {
|
|
325
|
-
raw: `${
|
|
342
|
+
raw: `${[
|
|
343
|
+
...headers,
|
|
344
|
+
`Content-Type: multipart/alternative; boundary="${boundary}"`
|
|
345
|
+
].join(CRLF)}${CRLF}${CRLF}` + `${part(text)}${part(html)}--${boundary}--${CRLF}`,
|
|
326
346
|
messageId,
|
|
327
347
|
subject: message.subject.trim()
|
|
328
348
|
};
|
|
329
349
|
}
|
|
350
|
+
/**
|
|
351
|
+
* One body with its content headers: `7bit` when it is ASCII with no line
|
|
352
|
+
* over 998 octets, `quoted-printable` otherwise. The body ends with a CRLF,
|
|
353
|
+
* which is what puts a following boundary on a line of its own.
|
|
354
|
+
*/ function mimePart(type, content) {
|
|
355
|
+
const body = canonicalBody(content);
|
|
356
|
+
const sevenBit = isSevenBitSafe(body);
|
|
357
|
+
return {
|
|
358
|
+
headers: [
|
|
359
|
+
`Content-Type: ${type}; charset=UTF-8`,
|
|
360
|
+
`Content-Transfer-Encoding: ${sevenBit ? '7bit' : 'quoted-printable'}`
|
|
361
|
+
],
|
|
362
|
+
body: sevenBit ? body : encodeQuotedPrintable(body)
|
|
363
|
+
};
|
|
364
|
+
}
|
|
330
365
|
/** The `raw` field Gmail's `messages.send` takes: the message, base64url. */ export function encodeGmailRawMessage(raw) {
|
|
331
366
|
return Buffer.from(raw, 'utf8').toString('base64url');
|
|
332
367
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/transport/rfc5322.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { LIST_UNSUBSCRIBE_ONE_CLICK } from '@aglyn/shared-util-email/list-unsubscribe'\nimport { randomUUID } from 'node:crypto'\n\n/**\n * AN OUTREACH MESSAGE AS RFC 5322 TEXT (AGL-2978).\n *\n * Gmail's `messages.send` takes the whole message, headers and body, as one\n * base64url string. This module writes that string for the one kind of mail\n * Outreach sends: a plain-text message from one person to one person.\n *\n * ## What it writes, and what it never does\n *\n * - `From` and `To` with display names — quoted when ASCII, RFC 2047\n * encoded-words when not.\n * - `Subject`, RFC 2047 when it is not plain ASCII, folded under 78 columns.\n * - `Date` in RFC 5322 form, UTC.\n * - `Message-ID` `<uuid@domain>`, the domain being the From address's unless\n * the caller names one, so the id belongs to the domain the mail is from.\n * - `MIME-Version: 1.0` and `Content-Type: text/plain; charset=UTF-8`.\n * - The caller's `In-Reply-To`, `References`, `Reply-To`, `List-Unsubscribe`\n * and `List-Unsubscribe-Post`, plus any `X-` header.\n * - CRLF line endings throughout. The body goes as `7bit` when it is ASCII\n * with no line over 998 octets, and `quoted-printable` otherwise.\n *\n * No HTML part, no tracking pixel, no rewritten links, no open or click\n * beacon of any kind: the body the caller composed is the body that is sent.\n *\n * ## Refusals\n *\n * A header value containing a line break is a header INJECTION, whoever typed\n * it — a merge field holding a CRLF would append headers of its choosing — so\n * every value is checked and a violation throws {@link Rfc5322MessageError}\n * rather than being cleaned up. Structural headers (`From`, `To`, `Cc`,\n * `Bcc`, `Subject`, `Date`, `Message-ID`, the MIME fields, …) cannot be passed\n * in `headers`: this module writes them, exactly once.\n *\n * Addresses must be ASCII. An internationalized mailbox needs SMTPUTF8 end to\n * end, which a one-to-one prospecting message cannot count on, so one is\n * refused rather than sent in a form some hop would mangle.\n */\n\n/** One mailbox, optionally with the name people see. */\nexport interface OutreachMailAddress {\n address: string\n name?: string | null\n}\n\n/** An address as a bare string, or with a display name. */\nexport type OutreachMailAddressInput = string | OutreachMailAddress\n\n/**\n * A composed message, as the sequence engine hands it over: who it is from,\n * who it is to, the subject, the plain-text body, and any extra headers.\n */\nexport interface OutreachComposedMessage {\n from: OutreachMailAddressInput\n to: OutreachMailAddressInput\n subject: string\n text: string\n /**\n * `In-Reply-To`, `References`, `Reply-To`, `List-Unsubscribe`,\n * `List-Unsubscribe-Post`, or an `X-` header. A null, undefined or empty\n * value is omitted.\n */\n headers?: Readonly<Record<string, string | null | undefined>>\n}\n\nexport interface BuildRfc5322Options {\n /** The `Date` header. Now when omitted. */\n date?: Date\n /** The Message-ID's left part. A random UUID when omitted. */\n messageIdLocalPart?: string\n /** The Message-ID's domain. The From address's domain when omitted. */\n messageIdDomain?: string\n}\n\nexport interface BuiltRfc5322Message {\n /** The message, CRLF line endings, ready to base64url-encode. */\n raw: string\n /** The `Message-ID`, angle brackets included, to thread replies against. */\n messageId: string\n /** The subject as written, trimmed: what the recipient sees. */\n subject: string\n}\n\nexport type Rfc5322MessageErrorCode =\n | 'invalid-address'\n | 'invalid-subject'\n | 'invalid-header'\n | 'reserved-header'\n | 'unsupported-header'\n | 'invalid-message-id'\n\nexport class Rfc5322MessageError extends Error {\n readonly code: Rfc5322MessageErrorCode\n\n constructor(code: Rfc5322MessageErrorCode, message: string) {\n super(message)\n this.name = 'Rfc5322MessageError'\n this.code = code\n }\n}\n\nconst CRLF = '\\r\\n'\n\n/** RFC 5322 recommends lines of at most 78 characters. */\nconst FOLD_AT = 78\n\n/** RFC 5322's hard limit on a line, excluding the CRLF. */\nconst MAX_LINE = 998\n\n/** A pragmatic ASCII addr-spec: no whitespace, specials or control characters. */\nconst ADDRESS = /^[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]+@[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?(?:\\.[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?)+$/\n\n/** A header field name: printable ASCII, no colon (RFC 5322 §3.6.8). */\nconst FIELD_NAME = /^[!-9;-~]+$/\n\n/**\n * Whether text holds a character below space other than tab, or DEL. Finding\n * one is the point: a CR or LF in a header value is a header injection.\n */\nfunction hasControlCharacter(value: string): boolean {\n for (let index = 0; index < value.length; index += 1) {\n const code = value.charCodeAt(index)\n if ((code < 32 && code !== 9) || code === 127) return true\n }\n return false\n}\n\n/** Whether text is printable ASCII and tabs only. */\nfunction isPrintableAscii(value: string): boolean {\n for (let index = 0; index < value.length; index += 1) {\n const code = value.charCodeAt(index)\n if (code !== 9 && (code < 32 || code > 126)) return false\n }\n return true\n}\n\n/** A Message-ID's left part, kept to the characters a UUID and a caller need. */\nconst MESSAGE_ID_LOCAL = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/\n\n/**\n * Headers the builder writes itself. Passing one in `headers` is refused, so\n * no message can carry two `From` fields or smuggle a second `Content-Type`.\n */\nconst RESERVED_HEADERS = new Set([\n 'from',\n 'sender',\n 'to',\n 'cc',\n 'bcc',\n 'subject',\n 'date',\n 'message-id',\n 'mime-version',\n 'content-type',\n 'content-transfer-encoding',\n 'content-disposition',\n 'return-path',\n 'received',\n 'dkim-signature',\n])\n\n/** The extra headers a caller may set, spelled as they are written. */\nconst EXTRA_HEADERS: Record<string, string> = {\n 'in-reply-to': 'In-Reply-To',\n references: 'References',\n 'reply-to': 'Reply-To',\n 'list-unsubscribe': 'List-Unsubscribe',\n 'list-unsubscribe-post': 'List-Unsubscribe-Post',\n}\n\n/**\n * The one value RFC 8058 defines for `List-Unsubscribe-Post`, re-exported\n * from the module campaigns and sequences share (AGL-3307).\n */\nexport { LIST_UNSUBSCRIBE_ONE_CLICK }\n\nconst isAscii = (value: string) => isPrintableAscii(value)\n\n/** The address, validated and in its written form. */\nfunction normalizeAddress(input: OutreachMailAddressInput, field: string): OutreachMailAddress {\n const record = typeof input === 'string' ? { address: input } : input\n const address = String(record?.address ?? '').trim()\n if (!address || address.length > 254 || !ADDRESS.test(address)) {\n throw new Rfc5322MessageError('invalid-address', `The ${field} address is not a valid ASCII email address.`)\n }\n const name = typeof record?.name === 'string' ? record.name.trim() : ''\n if (hasControlCharacter(name)) {\n throw new Rfc5322MessageError('invalid-address', `The ${field} display name contains a control character.`)\n }\n return { address, name }\n}\n\n/**\n * RFC 2047 `B` encoded-words for a UTF-8 string, each at most 75 characters\n * and never splitting a character between two words.\n */\nexport function encodeRfc2047Words(value: string): string[] {\n // 42 bytes base64-encode to 56 characters; with `=?UTF-8?B?` and `?=` a\n // word is 68 — inside the 75 the RFC allows, and short enough that the\n // first word still fits on a `Subject: ` line within 78 columns.\n const maxBytes = 42\n const words: string[] = []\n let chunk = ''\n let chunkBytes = 0\n for (const character of value) {\n const bytes = Buffer.byteLength(character, 'utf8')\n if (chunkBytes + bytes > maxBytes && chunk) {\n words.push(chunk)\n chunk = ''\n chunkBytes = 0\n }\n chunk += character\n chunkBytes += bytes\n }\n if (chunk) words.push(chunk)\n return words.map((word) => `=?UTF-8?B?${Buffer.from(word, 'utf8').toString('base64')}?=`)\n}\n\n/** A quoted-string, escaping the two characters RFC 5322 requires. */\nconst quoted = (value: string) => `\"${value.replace(/([\"\\\\])/g, '\\\\$1')}\"`\n\n/**\n * Whether ASCII text would read as an encoded-word to a mail client, which\n * decodes `=?…?=` wherever it appears in a header.\n */\nconst looksEncoded = (value: string) => /=\\?[^?]*\\?[bBqQ]\\?[^?]*\\?=/.test(value)\n\n/**\n * `Name <address>` as tokens to fold, the name quoted or encoded as it needs.\n * The angle-address is its own token, so a long name folds before it.\n */\nfunction formatAddress(address: OutreachMailAddress): string[] {\n if (!address.name) return [address.address]\n const name =\n isAscii(address.name) && !looksEncoded(address.name)\n ? [quoted(address.name)]\n : encodeRfc2047Words(address.name)\n return [...name, `<${address.address}>`]\n}\n\n/**\n * A value split at single spaces for folding, with every run of spaces kept:\n * an empty token is merged into the next one, so joining the tokens with one\n * space gives the value back exactly and no fold ever leaves a line of only\n * whitespace.\n */\nfunction spaceTokens(value: string): string[] {\n const tokens: string[] = []\n let pending = ''\n for (const part of value.split(' ')) {\n if (!part) {\n pending += ' '\n continue\n }\n tokens.push(`${pending}${part}`)\n pending = ''\n }\n return tokens\n}\n\n/**\n * One header written as folded lines: `tokens` are joined by a space, and a\n * token that would push its line past 78 columns starts the next line.\n */\nfunction foldHeader(name: string, tokens: readonly string[]): string {\n const lines: string[] = []\n let line = `${name}:`\n for (const token of tokens) {\n if (line.length + 1 + token.length > FOLD_AT && line !== `${name}:`) {\n lines.push(line)\n line = ` ${token}`\n } else {\n line += ` ${token}`\n }\n }\n lines.push(line)\n for (const written of lines) {\n if (written.length > MAX_LINE) {\n throw new Rfc5322MessageError('invalid-header', `The ${name} header has a line longer than ${MAX_LINE} characters.`)\n }\n }\n return lines.join(CRLF)\n}\n\n/** The subject, as words to fold: plain ASCII split on spaces, or encoded-words. */\nfunction subjectTokens(subject: string): string[] {\n if (typeof subject !== 'string') {\n throw new Rfc5322MessageError('invalid-subject', 'The subject must be text.')\n }\n if (hasControlCharacter(subject)) {\n throw new Rfc5322MessageError('invalid-subject', 'The subject contains a line break or control character.')\n }\n const trimmed = subject.trim()\n if (!trimmed) return []\n if (isAscii(trimmed) && !looksEncoded(trimmed)) {\n const words = spaceTokens(trimmed)\n if (words.every((word) => `Subject: ${word}`.length <= MAX_LINE)) return words\n }\n return encodeRfc2047Words(trimmed)\n}\n\n/** RFC 5322 §3.3 date-time, in UTC. */\nexport function formatRfc5322Date(date: Date): string {\n const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']\n const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']\n const pad = (value: number) => String(value).padStart(2, '0')\n return (\n `${days[date.getUTCDay()]}, ${pad(date.getUTCDate())} ${months[date.getUTCMonth()]} ` +\n `${date.getUTCFullYear()} ${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}:` +\n `${pad(date.getUTCSeconds())} +0000`\n )\n}\n\n/** The extra headers, validated, named as written, in the order given. */\nfunction extraHeaders(headers: OutreachComposedMessage['headers']): Array<[string, string]> {\n const written: Array<[string, string]> = []\n const seen = new Set<string>()\n for (const [rawName, rawValue] of Object.entries(headers ?? {})) {\n if (rawValue === null || rawValue === undefined || rawValue === '') continue\n const name = String(rawName).trim()\n const lower = name.toLowerCase()\n if (!FIELD_NAME.test(name)) {\n throw new Rfc5322MessageError('invalid-header', 'A header name is not a valid field name.')\n }\n if (RESERVED_HEADERS.has(lower)) {\n throw new Rfc5322MessageError('reserved-header', `The ${name} header is written by the message builder and cannot be set.`)\n }\n const canonical = EXTRA_HEADERS[lower] ?? (lower.startsWith('x-') ? name : null)\n if (!canonical) {\n throw new Rfc5322MessageError('unsupported-header', `The ${name} header is not one a sequence sends.`)\n }\n if (seen.has(lower)) {\n throw new Rfc5322MessageError('invalid-header', `The ${name} header is given twice.`)\n }\n const value = String(rawValue).trim()\n if (hasControlCharacter(value) || !isAscii(value)) {\n throw new Rfc5322MessageError('invalid-header', `The ${name} header value must be printable ASCII on one line.`)\n }\n seen.add(lower)\n written.push([canonical, value])\n }\n const post = written.find(([name]) => name === 'List-Unsubscribe-Post')\n if (post) {\n const unsubscribe = written.find(([name]) => name === 'List-Unsubscribe')\n if (post[1] !== LIST_UNSUBSCRIBE_ONE_CLICK || !unsubscribe || !/<https:\\/\\/[^>]+>/.test(unsubscribe[1])) {\n throw new Rfc5322MessageError(\n 'invalid-header',\n `List-Unsubscribe-Post must be \"${LIST_UNSUBSCRIBE_ONE_CLICK}\" beside a List-Unsubscribe with an https address (RFC 8058).`,\n )\n }\n }\n return written\n}\n\n/** Line breaks of any kind, as CRLF, ending with one. */\nfunction canonicalBody(text: string): string {\n const normalized = String(text ?? '').replace(/\\r\\n|\\r|\\n/g, CRLF)\n return normalized.endsWith(CRLF) ? normalized : `${normalized}${CRLF}`\n}\n\n/**\n * RFC 2045 §6.7 quoted-printable for CRLF-separated text: `=`, non-ASCII\n * bytes and control characters as `=XX`, whitespace at a line's end encoded,\n * and soft breaks keeping every encoded line to 76 characters.\n */\nexport function encodeQuotedPrintable(text: string): string {\n const hex = (byte: number) => `=${byte.toString(16).toUpperCase().padStart(2, '0')}`\n return text\n .split(CRLF)\n .map((line) => {\n const bytes = Buffer.from(line, 'utf8')\n let encoded = ''\n let current = ''\n bytes.forEach((byte, index) => {\n const last = index === bytes.length - 1\n const literal =\n (byte >= 33 && byte <= 126 && byte !== 61) || ((byte === 32 || byte === 9) && !last)\n const token = literal ? String.fromCharCode(byte) : hex(byte)\n if (current.length + token.length > 75) {\n encoded += `${current}=${CRLF}`\n current = ''\n }\n current += token\n })\n return encoded + current\n })\n .join(CRLF)\n}\n\n/** Whether the body can travel as 7bit: ASCII, and no line over 998 octets. */\nfunction isSevenBitSafe(body: string): boolean {\n return body.split(CRLF).every((line) => isAscii(line) && line.length <= MAX_LINE)\n}\n\n/** Builds the message. Throws {@link Rfc5322MessageError} on anything unsafe. */\nexport function buildRfc5322Message(\n message: OutreachComposedMessage,\n options: BuildRfc5322Options = {},\n): BuiltRfc5322Message {\n const from = normalizeAddress(message.from, 'From')\n const to = normalizeAddress(message.to, 'To')\n const subject = subjectTokens(message.subject)\n const extras = extraHeaders(message.headers)\n\n const domain = (options.messageIdDomain ?? from.address.slice(from.address.lastIndexOf('@') + 1)).trim()\n const local = options.messageIdLocalPart ?? randomUUID()\n if (!MESSAGE_ID_LOCAL.test(local) || !ADDRESS.test(`id@${domain}`)) {\n throw new Rfc5322MessageError('invalid-message-id', 'The Message-ID would not be valid.')\n }\n const messageId = `<${local}@${domain}>`\n\n const body = canonicalBody(message.text)\n const sevenBit = isSevenBitSafe(body)\n\n const lines = [\n foldHeader('From', formatAddress(from)),\n foldHeader('To', formatAddress(to)),\n ...(subject.length ? [foldHeader('Subject', subject)] : ['Subject:']),\n `Date: ${formatRfc5322Date(options.date ?? new Date())}`,\n `Message-ID: ${messageId}`,\n ...extras.map(([name, value]) => foldHeader(name, spaceTokens(value))),\n 'MIME-Version: 1.0',\n 'Content-Type: text/plain; charset=UTF-8',\n `Content-Transfer-Encoding: ${sevenBit ? '7bit' : 'quoted-printable'}`,\n ]\n const encodedBody = sevenBit ? body : encodeQuotedPrintable(body)\n return {\n raw: `${lines.join(CRLF)}${CRLF}${CRLF}${encodedBody}`,\n messageId,\n subject: message.subject.trim(),\n }\n}\n\n/** The `raw` field Gmail's `messages.send` takes: the message, base64url. */\nexport function encodeGmailRawMessage(raw: string): string {\n return Buffer.from(raw, 'utf8').toString('base64url')\n}\n"],"names":["LIST_UNSUBSCRIBE_ONE_CLICK","randomUUID","Rfc5322MessageError","Error","code","message","name","CRLF","FOLD_AT","MAX_LINE","ADDRESS","FIELD_NAME","hasControlCharacter","value","index","length","charCodeAt","isPrintableAscii","MESSAGE_ID_LOCAL","RESERVED_HEADERS","Set","EXTRA_HEADERS","references","isAscii","normalizeAddress","input","field","record","address","String","trim","test","encodeRfc2047Words","maxBytes","words","chunk","chunkBytes","character","bytes","Buffer","byteLength","push","map","word","from","toString","quoted","replace","looksEncoded","formatAddress","spaceTokens","tokens","pending","part","split","foldHeader","lines","line","token","written","join","subjectTokens","subject","trimmed","every","formatRfc5322Date","date","days","months","pad","padStart","getUTCDay","getUTCDate","getUTCMonth","getUTCFullYear","getUTCHours","getUTCMinutes","getUTCSeconds","extraHeaders","headers","seen","rawName","rawValue","Object","entries","undefined","lower","toLowerCase","has","canonical","startsWith","add","post","find","unsubscribe","canonicalBody","text","normalized","endsWith","encodeQuotedPrintable","hex","byte","toUpperCase","encoded","current","forEach","last","literal","fromCharCode","isSevenBitSafe","body","buildRfc5322Message","options","to","extras","domain","messageIdDomain","slice","lastIndexOf","local","messageIdLocalPart","messageId","sevenBit","Date","encodedBody","raw","encodeGmailRawMessage"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/transport/rfc5322.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { LIST_UNSUBSCRIBE_ONE_CLICK } from '@aglyn/shared-util-email/list-unsubscribe'\nimport { randomUUID } from 'node:crypto'\n\n/**\n * AN OUTREACH MESSAGE AS RFC 5322 TEXT (AGL-2978).\n *\n * Gmail's `messages.send` takes the whole message, headers and body, as one\n * base64url string. This module writes that string for the one kind of mail\n * Outreach sends: a plain-text message from one person to one person.\n *\n * ## What it writes, and what it never does\n *\n * - `From` and `To` with display names — quoted when ASCII, RFC 2047\n * encoded-words when not.\n * - `Subject`, RFC 2047 when it is not plain ASCII, folded under 78 columns.\n * - `Date` in RFC 5322 form, UTC.\n * - `Message-ID` `<uuid@domain>`, the domain being the From address's unless\n * the caller names one, so the id belongs to the domain the mail is from.\n * - `MIME-Version: 1.0` and `Content-Type: text/plain; charset=UTF-8` —\n * or, for a sequence that counts opens (AGL-3395), `multipart/alternative`\n * holding that same plain-text part and the caller's HTML part after it.\n * - The caller's `In-Reply-To`, `References`, `Reply-To`, `List-Unsubscribe`\n * and `List-Unsubscribe-Post`, plus any `X-` header.\n * - CRLF line endings throughout. The body goes as `7bit` when it is ASCII\n * with no line over 998 octets, and `quoted-printable` otherwise.\n *\n * Nothing is added to what the caller composed: no HTML part it did not\n * pass, no image, no rewritten link. The body the caller composed is the\n * body that is sent.\n *\n * ## Refusals\n *\n * A header value containing a line break is a header INJECTION, whoever typed\n * it — a merge field holding a CRLF would append headers of its choosing — so\n * every value is checked and a violation throws {@link Rfc5322MessageError}\n * rather than being cleaned up. Structural headers (`From`, `To`, `Cc`,\n * `Bcc`, `Subject`, `Date`, `Message-ID`, the MIME fields, …) cannot be passed\n * in `headers`: this module writes them, exactly once.\n *\n * Addresses must be ASCII. An internationalized mailbox needs SMTPUTF8 end to\n * end, which a one-to-one prospecting message cannot count on, so one is\n * refused rather than sent in a form some hop would mangle.\n */\n\n/** One mailbox, optionally with the name people see. */\nexport interface OutreachMailAddress {\n address: string\n name?: string | null\n}\n\n/** An address as a bare string, or with a display name. */\nexport type OutreachMailAddressInput = string | OutreachMailAddress\n\n/**\n * A composed message, as the sequence engine hands it over: who it is from,\n * who it is to, the subject, the plain-text body, and any extra headers.\n */\nexport interface OutreachComposedMessage {\n from: OutreachMailAddressInput\n to: OutreachMailAddressInput\n subject: string\n text: string\n /**\n * An HTML alternative to `text` (AGL-3395). Absent — every send of a\n * sequence that does not count opens — sends `text` alone as\n * `text/plain`, exactly as before.\n */\n html?: string | null\n /**\n * `In-Reply-To`, `References`, `Reply-To`, `List-Unsubscribe`,\n * `List-Unsubscribe-Post`, or an `X-` header. A null, undefined or empty\n * value is omitted.\n */\n headers?: Readonly<Record<string, string | null | undefined>>\n}\n\nexport interface BuildRfc5322Options {\n /** The `Date` header. Now when omitted. */\n date?: Date\n /** The Message-ID's left part. A random UUID when omitted. */\n messageIdLocalPart?: string\n /** The Message-ID's domain. The From address's domain when omitted. */\n messageIdDomain?: string\n /** The `multipart/alternative` boundary. A random one when omitted. */\n boundary?: string\n}\n\nexport interface BuiltRfc5322Message {\n /** The message, CRLF line endings, ready to base64url-encode. */\n raw: string\n /** The `Message-ID`, angle brackets included, to thread replies against. */\n messageId: string\n /** The subject as written, trimmed: what the recipient sees. */\n subject: string\n}\n\nexport type Rfc5322MessageErrorCode =\n | 'invalid-address'\n | 'invalid-subject'\n | 'invalid-header'\n | 'reserved-header'\n | 'unsupported-header'\n | 'invalid-message-id'\n\nexport class Rfc5322MessageError extends Error {\n readonly code: Rfc5322MessageErrorCode\n\n constructor(code: Rfc5322MessageErrorCode, message: string) {\n super(message)\n this.name = 'Rfc5322MessageError'\n this.code = code\n }\n}\n\nconst CRLF = '\\r\\n'\n\n/** RFC 5322 recommends lines of at most 78 characters. */\nconst FOLD_AT = 78\n\n/** RFC 5322's hard limit on a line, excluding the CRLF. */\nconst MAX_LINE = 998\n\n/** A pragmatic ASCII addr-spec: no whitespace, specials or control characters. */\nconst ADDRESS = /^[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]+@[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?(?:\\.[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?)+$/\n\n/** A header field name: printable ASCII, no colon (RFC 5322 §3.6.8). */\nconst FIELD_NAME = /^[!-9;-~]+$/\n\n/**\n * Whether text holds a character below space other than tab, or DEL. Finding\n * one is the point: a CR or LF in a header value is a header injection.\n */\nfunction hasControlCharacter(value: string): boolean {\n for (let index = 0; index < value.length; index += 1) {\n const code = value.charCodeAt(index)\n if ((code < 32 && code !== 9) || code === 127) return true\n }\n return false\n}\n\n/** Whether text is printable ASCII and tabs only. */\nfunction isPrintableAscii(value: string): boolean {\n for (let index = 0; index < value.length; index += 1) {\n const code = value.charCodeAt(index)\n if (code !== 9 && (code < 32 || code > 126)) return false\n }\n return true\n}\n\n/** A Message-ID's left part, kept to the characters a UUID and a caller need. */\nconst MESSAGE_ID_LOCAL = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/\n\n/**\n * Headers the builder writes itself. Passing one in `headers` is refused, so\n * no message can carry two `From` fields or smuggle a second `Content-Type`.\n */\nconst RESERVED_HEADERS = new Set([\n 'from',\n 'sender',\n 'to',\n 'cc',\n 'bcc',\n 'subject',\n 'date',\n 'message-id',\n 'mime-version',\n 'content-type',\n 'content-transfer-encoding',\n 'content-disposition',\n 'return-path',\n 'received',\n 'dkim-signature',\n])\n\n/** The extra headers a caller may set, spelled as they are written. */\nconst EXTRA_HEADERS: Record<string, string> = {\n 'in-reply-to': 'In-Reply-To',\n references: 'References',\n 'reply-to': 'Reply-To',\n 'list-unsubscribe': 'List-Unsubscribe',\n 'list-unsubscribe-post': 'List-Unsubscribe-Post',\n}\n\n/**\n * The one value RFC 8058 defines for `List-Unsubscribe-Post`, re-exported\n * from the module campaigns and sequences share (AGL-3307).\n */\nexport { LIST_UNSUBSCRIBE_ONE_CLICK }\n\nconst isAscii = (value: string) => isPrintableAscii(value)\n\n/** The address, validated and in its written form. */\nfunction normalizeAddress(input: OutreachMailAddressInput, field: string): OutreachMailAddress {\n const record = typeof input === 'string' ? { address: input } : input\n const address = String(record?.address ?? '').trim()\n if (!address || address.length > 254 || !ADDRESS.test(address)) {\n throw new Rfc5322MessageError('invalid-address', `The ${field} address is not a valid ASCII email address.`)\n }\n const name = typeof record?.name === 'string' ? record.name.trim() : ''\n if (hasControlCharacter(name)) {\n throw new Rfc5322MessageError('invalid-address', `The ${field} display name contains a control character.`)\n }\n return { address, name }\n}\n\n/**\n * RFC 2047 `B` encoded-words for a UTF-8 string, each at most 75 characters\n * and never splitting a character between two words.\n */\nexport function encodeRfc2047Words(value: string): string[] {\n // 42 bytes base64-encode to 56 characters; with `=?UTF-8?B?` and `?=` a\n // word is 68 — inside the 75 the RFC allows, and short enough that the\n // first word still fits on a `Subject: ` line within 78 columns.\n const maxBytes = 42\n const words: string[] = []\n let chunk = ''\n let chunkBytes = 0\n for (const character of value) {\n const bytes = Buffer.byteLength(character, 'utf8')\n if (chunkBytes + bytes > maxBytes && chunk) {\n words.push(chunk)\n chunk = ''\n chunkBytes = 0\n }\n chunk += character\n chunkBytes += bytes\n }\n if (chunk) words.push(chunk)\n return words.map((word) => `=?UTF-8?B?${Buffer.from(word, 'utf8').toString('base64')}?=`)\n}\n\n/** A quoted-string, escaping the two characters RFC 5322 requires. */\nconst quoted = (value: string) => `\"${value.replace(/([\"\\\\])/g, '\\\\$1')}\"`\n\n/**\n * Whether ASCII text would read as an encoded-word to a mail client, which\n * decodes `=?…?=` wherever it appears in a header.\n */\nconst looksEncoded = (value: string) => /=\\?[^?]*\\?[bBqQ]\\?[^?]*\\?=/.test(value)\n\n/**\n * `Name <address>` as tokens to fold, the name quoted or encoded as it needs.\n * The angle-address is its own token, so a long name folds before it.\n */\nfunction formatAddress(address: OutreachMailAddress): string[] {\n if (!address.name) return [address.address]\n const name =\n isAscii(address.name) && !looksEncoded(address.name)\n ? [quoted(address.name)]\n : encodeRfc2047Words(address.name)\n return [...name, `<${address.address}>`]\n}\n\n/**\n * A value split at single spaces for folding, with every run of spaces kept:\n * an empty token is merged into the next one, so joining the tokens with one\n * space gives the value back exactly and no fold ever leaves a line of only\n * whitespace.\n */\nfunction spaceTokens(value: string): string[] {\n const tokens: string[] = []\n let pending = ''\n for (const part of value.split(' ')) {\n if (!part) {\n pending += ' '\n continue\n }\n tokens.push(`${pending}${part}`)\n pending = ''\n }\n return tokens\n}\n\n/**\n * One header written as folded lines: `tokens` are joined by a space, and a\n * token that would push its line past 78 columns starts the next line.\n */\nfunction foldHeader(name: string, tokens: readonly string[]): string {\n const lines: string[] = []\n let line = `${name}:`\n for (const token of tokens) {\n if (line.length + 1 + token.length > FOLD_AT && line !== `${name}:`) {\n lines.push(line)\n line = ` ${token}`\n } else {\n line += ` ${token}`\n }\n }\n lines.push(line)\n for (const written of lines) {\n if (written.length > MAX_LINE) {\n throw new Rfc5322MessageError('invalid-header', `The ${name} header has a line longer than ${MAX_LINE} characters.`)\n }\n }\n return lines.join(CRLF)\n}\n\n/** The subject, as words to fold: plain ASCII split on spaces, or encoded-words. */\nfunction subjectTokens(subject: string): string[] {\n if (typeof subject !== 'string') {\n throw new Rfc5322MessageError('invalid-subject', 'The subject must be text.')\n }\n if (hasControlCharacter(subject)) {\n throw new Rfc5322MessageError('invalid-subject', 'The subject contains a line break or control character.')\n }\n const trimmed = subject.trim()\n if (!trimmed) return []\n if (isAscii(trimmed) && !looksEncoded(trimmed)) {\n const words = spaceTokens(trimmed)\n if (words.every((word) => `Subject: ${word}`.length <= MAX_LINE)) return words\n }\n return encodeRfc2047Words(trimmed)\n}\n\n/** RFC 5322 §3.3 date-time, in UTC. */\nexport function formatRfc5322Date(date: Date): string {\n const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']\n const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']\n const pad = (value: number) => String(value).padStart(2, '0')\n return (\n `${days[date.getUTCDay()]}, ${pad(date.getUTCDate())} ${months[date.getUTCMonth()]} ` +\n `${date.getUTCFullYear()} ${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}:` +\n `${pad(date.getUTCSeconds())} +0000`\n )\n}\n\n/** The extra headers, validated, named as written, in the order given. */\nfunction extraHeaders(headers: OutreachComposedMessage['headers']): Array<[string, string]> {\n const written: Array<[string, string]> = []\n const seen = new Set<string>()\n for (const [rawName, rawValue] of Object.entries(headers ?? {})) {\n if (rawValue === null || rawValue === undefined || rawValue === '') continue\n const name = String(rawName).trim()\n const lower = name.toLowerCase()\n if (!FIELD_NAME.test(name)) {\n throw new Rfc5322MessageError('invalid-header', 'A header name is not a valid field name.')\n }\n if (RESERVED_HEADERS.has(lower)) {\n throw new Rfc5322MessageError('reserved-header', `The ${name} header is written by the message builder and cannot be set.`)\n }\n const canonical = EXTRA_HEADERS[lower] ?? (lower.startsWith('x-') ? name : null)\n if (!canonical) {\n throw new Rfc5322MessageError('unsupported-header', `The ${name} header is not one a sequence sends.`)\n }\n if (seen.has(lower)) {\n throw new Rfc5322MessageError('invalid-header', `The ${name} header is given twice.`)\n }\n const value = String(rawValue).trim()\n if (hasControlCharacter(value) || !isAscii(value)) {\n throw new Rfc5322MessageError('invalid-header', `The ${name} header value must be printable ASCII on one line.`)\n }\n seen.add(lower)\n written.push([canonical, value])\n }\n const post = written.find(([name]) => name === 'List-Unsubscribe-Post')\n if (post) {\n const unsubscribe = written.find(([name]) => name === 'List-Unsubscribe')\n if (post[1] !== LIST_UNSUBSCRIBE_ONE_CLICK || !unsubscribe || !/<https:\\/\\/[^>]+>/.test(unsubscribe[1])) {\n throw new Rfc5322MessageError(\n 'invalid-header',\n `List-Unsubscribe-Post must be \"${LIST_UNSUBSCRIBE_ONE_CLICK}\" beside a List-Unsubscribe with an https address (RFC 8058).`,\n )\n }\n }\n return written\n}\n\n/** Line breaks of any kind, as CRLF, ending with one. */\nfunction canonicalBody(text: string): string {\n const normalized = String(text ?? '').replace(/\\r\\n|\\r|\\n/g, CRLF)\n return normalized.endsWith(CRLF) ? normalized : `${normalized}${CRLF}`\n}\n\n/**\n * RFC 2045 §6.7 quoted-printable for CRLF-separated text: `=`, non-ASCII\n * bytes and control characters as `=XX`, whitespace at a line's end encoded,\n * and soft breaks keeping every encoded line to 76 characters.\n */\nexport function encodeQuotedPrintable(text: string): string {\n const hex = (byte: number) => `=${byte.toString(16).toUpperCase().padStart(2, '0')}`\n return text\n .split(CRLF)\n .map((line) => {\n const bytes = Buffer.from(line, 'utf8')\n let encoded = ''\n let current = ''\n bytes.forEach((byte, index) => {\n const last = index === bytes.length - 1\n const literal =\n (byte >= 33 && byte <= 126 && byte !== 61) || ((byte === 32 || byte === 9) && !last)\n const token = literal ? String.fromCharCode(byte) : hex(byte)\n if (current.length + token.length > 75) {\n encoded += `${current}=${CRLF}`\n current = ''\n }\n current += token\n })\n return encoded + current\n })\n .join(CRLF)\n}\n\n/** Whether the body can travel as 7bit: ASCII, and no line over 998 octets. */\nfunction isSevenBitSafe(body: string): boolean {\n return body.split(CRLF).every((line) => isAscii(line) && line.length <= MAX_LINE)\n}\n\n/** Builds the message. Throws {@link Rfc5322MessageError} on anything unsafe. */\nexport function buildRfc5322Message(\n message: OutreachComposedMessage,\n options: BuildRfc5322Options = {},\n): BuiltRfc5322Message {\n const from = normalizeAddress(message.from, 'From')\n const to = normalizeAddress(message.to, 'To')\n const subject = subjectTokens(message.subject)\n const extras = extraHeaders(message.headers)\n\n const domain = (options.messageIdDomain ?? from.address.slice(from.address.lastIndexOf('@') + 1)).trim()\n const local = options.messageIdLocalPart ?? randomUUID()\n if (!MESSAGE_ID_LOCAL.test(local) || !ADDRESS.test(`id@${domain}`)) {\n throw new Rfc5322MessageError('invalid-message-id', 'The Message-ID would not be valid.')\n }\n const messageId = `<${local}@${domain}>`\n\n const headers = [\n foldHeader('From', formatAddress(from)),\n foldHeader('To', formatAddress(to)),\n ...(subject.length ? [foldHeader('Subject', subject)] : ['Subject:']),\n `Date: ${formatRfc5322Date(options.date ?? new Date())}`,\n `Message-ID: ${messageId}`,\n ...extras.map(([name, value]) => foldHeader(name, spaceTokens(value))),\n 'MIME-Version: 1.0',\n ]\n const text = mimePart('text/plain', message.text)\n const html = typeof message.html === 'string' && message.html.trim() ? mimePart('text/html', message.html) : null\n if (!html) {\n return {\n raw: `${[...headers, ...text.headers].join(CRLF)}${CRLF}${CRLF}${text.body}`,\n messageId,\n subject: message.subject.trim(),\n }\n }\n /*\n * RFC 2046 §5.1.4: alternatives in increasing order of preference, so the\n * plain text comes first and a client that can show HTML shows the HTML.\n * The boundary is a UUID's characters behind a prefix, which no\n * quoted-printable or 7bit body line can contain in full.\n */\n const boundary = options.boundary ?? `aglyn-alt-${randomUUID()}`\n if (!/^[A-Za-z0-9'()+_,./:=?-]{1,70}$/.test(boundary)) {\n throw new Rfc5322MessageError('invalid-header', 'The multipart boundary would not be valid.')\n }\n const part = (built: { headers: string[]; body: string }) =>\n `--${boundary}${CRLF}${built.headers.join(CRLF)}${CRLF}${CRLF}${built.body}`\n return {\n raw:\n `${[...headers, `Content-Type: multipart/alternative; boundary=\"${boundary}\"`].join(CRLF)}${CRLF}${CRLF}` +\n `${part(text)}${part(html)}--${boundary}--${CRLF}`,\n messageId,\n subject: message.subject.trim(),\n }\n}\n\n/**\n * One body with its content headers: `7bit` when it is ASCII with no line\n * over 998 octets, `quoted-printable` otherwise. The body ends with a CRLF,\n * which is what puts a following boundary on a line of its own.\n */\nfunction mimePart(type: 'text/plain' | 'text/html', content: string): { headers: string[]; body: string } {\n const body = canonicalBody(content)\n const sevenBit = isSevenBitSafe(body)\n return {\n headers: [\n `Content-Type: ${type}; charset=UTF-8`,\n `Content-Transfer-Encoding: ${sevenBit ? '7bit' : 'quoted-printable'}`,\n ],\n body: sevenBit ? body : encodeQuotedPrintable(body),\n }\n}\n\n/** The `raw` field Gmail's `messages.send` takes: the message, base64url. */\nexport function encodeGmailRawMessage(raw: string): string {\n return Buffer.from(raw, 'utf8').toString('base64url')\n}\n"],"names":["LIST_UNSUBSCRIBE_ONE_CLICK","randomUUID","Rfc5322MessageError","Error","code","message","name","CRLF","FOLD_AT","MAX_LINE","ADDRESS","FIELD_NAME","hasControlCharacter","value","index","length","charCodeAt","isPrintableAscii","MESSAGE_ID_LOCAL","RESERVED_HEADERS","Set","EXTRA_HEADERS","references","isAscii","normalizeAddress","input","field","record","address","String","trim","test","encodeRfc2047Words","maxBytes","words","chunk","chunkBytes","character","bytes","Buffer","byteLength","push","map","word","from","toString","quoted","replace","looksEncoded","formatAddress","spaceTokens","tokens","pending","part","split","foldHeader","lines","line","token","written","join","subjectTokens","subject","trimmed","every","formatRfc5322Date","date","days","months","pad","padStart","getUTCDay","getUTCDate","getUTCMonth","getUTCFullYear","getUTCHours","getUTCMinutes","getUTCSeconds","extraHeaders","headers","seen","rawName","rawValue","Object","entries","undefined","lower","toLowerCase","has","canonical","startsWith","add","post","find","unsubscribe","canonicalBody","text","normalized","endsWith","encodeQuotedPrintable","hex","byte","toUpperCase","encoded","current","forEach","last","literal","fromCharCode","isSevenBitSafe","body","buildRfc5322Message","options","to","extras","domain","messageIdDomain","slice","lastIndexOf","local","messageIdLocalPart","messageId","Date","mimePart","html","raw","boundary","built","type","content","sevenBit","encodeGmailRawMessage"],"mappings":"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"}
|
|
@@ -54,7 +54,7 @@ import { buildRfc5322Message, encodeGmailRawMessage } from "./rfc5322.js";
|
|
|
54
54
|
* Answers the Gmail thread id, the `Message-ID` and the subject as sent,
|
|
55
55
|
* which the runtime records on the enrollment for the next step to answer.
|
|
56
56
|
*/ export function sendComposedOutreachEmail(client, email, sender, options = {}) {
|
|
57
|
-
var _unsubscribe_ListUnsubscribe, _unsubscribe_ListUnsubscribePost, _email_threadId;
|
|
57
|
+
var _email_html, _unsubscribe_ListUnsubscribe, _unsubscribe_ListUnsubscribePost, _email_threadId;
|
|
58
58
|
// The pair campaigns write too (AGL-3307): both URIs in one
|
|
59
59
|
// `List-Unsubscribe`, and the one-click `Post` only beside the URL.
|
|
60
60
|
const unsubscribe = listUnsubscribeHeaders({
|
|
@@ -66,6 +66,7 @@ import { buildRfc5322Message, encodeGmailRawMessage } from "./rfc5322.js";
|
|
|
66
66
|
to: email.to,
|
|
67
67
|
subject: email.subject,
|
|
68
68
|
text: email.text,
|
|
69
|
+
html: (_email_html = email.html) != null ? _email_html : null,
|
|
69
70
|
headers: {
|
|
70
71
|
'In-Reply-To': email.inReplyTo,
|
|
71
72
|
References: email.references,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/transport/send-message.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { ComposedOutreachEmail } from '../engine/compose'\nimport type { GmailClient } from './gmail-client'\nimport { listUnsubscribeHeaders } from '@aglyn/shared-util-email/list-unsubscribe'\nimport {\n buildRfc5322Message,\n encodeGmailRawMessage,\n type OutreachComposedMessage,\n type OutreachMailAddress,\n} from './rfc5322'\n\n/** What one sent message is known by afterwards. */\nexport interface OutreachSentMessage {\n /** Gmail's id for the message in the sender's mailbox. */\n gmailMessageId: string\n /** Gmail's thread id — what the next step of the sequence replies into. */\n threadId: string\n /** The RFC 5322 `Message-ID` it carried, for `In-Reply-To`/`References`. */\n messageId: string\n /**\n * The subject as sent — the thread's own subject when this message started\n * the thread, which a later step answers with `Re:`.\n */\n subject: string\n}\n\nexport interface SendOutreachMessageOptions {\n /** The Gmail thread to send into; a new thread when absent. */\n threadId?: string | null\n /** The `Date` header. Now when omitted. */\n date?: Date\n /** The Message-ID's domain. The From address's domain when omitted. */\n messageIdDomain?: string\n /**\n * The Message-ID's local part, minted by the caller so it knows the id\n * before the send answers (AGL-2981). A random one when omitted.\n */\n messageIdLocalPart?: string\n}\n\n/**\n * Builds a composed message and sends it through a mailbox's client\n * (AGL-2978). The one door a send goes through, so every Outreach message is\n * built by the same RFC 5322 writer and none can bypass its header checks.\n */\nexport async function sendOutreachMessage(\n client: Pick<GmailClient, 'sendMessage'>,\n message: OutreachComposedMessage,\n options: SendOutreachMessageOptions = {},\n): Promise<OutreachSentMessage> {\n const built = buildRfc5322Message(message, {\n date: options.date,\n messageIdDomain: options.messageIdDomain,\n messageIdLocalPart: options.messageIdLocalPart,\n })\n const sent = await client.sendMessage({\n raw: encodeGmailRawMessage(built.raw),\n threadId: options.threadId ?? null,\n })\n return {\n gmailMessageId: sent.id,\n threadId: sent.threadId,\n messageId: built.messageId,\n subject: built.subject,\n }\n}\n\n/**\n * Sends the email the engine composed (`composeOutreachEmail`) from a\n * mailbox, as the sending runtime does for a sequence step.\n *\n * The engine decides what is said and how it threads; this adds what the\n * engine leaves to the transport — the From line, from the mailbox's send-as\n * address and display name, and the MIME — and maps the rest onto headers:\n *\n * - a reply step goes into the engine's `threadId`, with its `In-Reply-To`\n * and `References`;\n * - the unsubscribe URL and mailto become one `List-Unsubscribe`, and the\n * URL adds `List-Unsubscribe-Post: List-Unsubscribe=One-Click` (RFC 8058).\n *\n * Answers the Gmail thread id, the `Message-ID` and the subject as sent,\n * which the runtime records on the enrollment for the next step to answer.\n */\nexport function sendComposedOutreachEmail(\n client: Pick<GmailClient, 'sendMessage'>,\n email: ComposedOutreachEmail,\n sender: OutreachMailAddress,\n options: Omit<SendOutreachMessageOptions, 'threadId'> = {},\n): Promise<OutreachSentMessage> {\n // The pair campaigns write too (AGL-3307): both URIs in one\n // `List-Unsubscribe`, and the one-click `Post` only beside the URL.\n const unsubscribe = listUnsubscribeHeaders({\n url: email.listUnsubscribeUrl,\n mailto: email.listUnsubscribeMailto,\n })\n return sendOutreachMessage(\n client,\n {\n from: sender,\n to: email.to,\n subject: email.subject,\n text: email.text,\n headers: {\n 'In-Reply-To': email.inReplyTo,\n References: email.references,\n 'List-Unsubscribe': unsubscribe['List-Unsubscribe'] ?? null,\n 'List-Unsubscribe-Post': unsubscribe['List-Unsubscribe-Post'] ?? null,\n },\n },\n { ...options, threadId: email.threadId ?? null },\n )\n}\n"],"names":["listUnsubscribeHeaders","buildRfc5322Message","encodeGmailRawMessage","sendOutreachMessage","client","message","options","built","date","messageIdDomain","messageIdLocalPart","sent","sendMessage","raw","threadId","gmailMessageId","id","messageId","subject","sendComposedOutreachEmail","email","sender","unsubscribe","url","listUnsubscribeUrl","mailto","listUnsubscribeMailto","from","to","text","headers","inReplyTo","References","references"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAID,SAASA,sBAAsB,QAAQ,4CAA2C;AAClF,SACEC,mBAAmB,EACnBC,qBAAqB,QAGhB,eAAW;AA+BlB;;;;CAIC,GACD,OAAO,eAAeC,oBACpBC,MAAwC,EACxCC,OAAgC,EAChCC,UAAsC,CAAC,CAAC;QAS5BA;IAPZ,MAAMC,QAAQN,oBAAoBI,SAAS;QACzCG,MAAMF,QAAQE,IAAI;QAClBC,iBAAiBH,QAAQG,eAAe;QACxCC,oBAAoBJ,QAAQI,kBAAkB;IAChD;IACA,MAAMC,OAAO,MAAMP,OAAOQ,WAAW,CAAC;QACpCC,KAAKX,sBAAsBK,MAAMM,GAAG;QACpCC,QAAQ,GAAER,oBAAAA,QAAQQ,QAAQ,YAAhBR,oBAAoB;IAChC;IACA,OAAO;QACLS,gBAAgBJ,KAAKK,EAAE;QACvBF,UAAUH,KAAKG,QAAQ;QACvBG,WAAWV,MAAMU,SAAS;QAC1BC,SAASX,MAAMW,OAAO;IACxB;AACF;AAEA;;;;;;;;;;;;;;;CAeC,GACD,OAAO,SAASC,0BACdf,MAAwC,EACxCgB,KAA4B,EAC5BC,MAA2B,EAC3Bf,UAAwD,CAAC,CAAC;
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/transport/send-message.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { ComposedOutreachEmail } from '../engine/compose'\nimport type { GmailClient } from './gmail-client'\nimport { listUnsubscribeHeaders } from '@aglyn/shared-util-email/list-unsubscribe'\nimport {\n buildRfc5322Message,\n encodeGmailRawMessage,\n type OutreachComposedMessage,\n type OutreachMailAddress,\n} from './rfc5322'\n\n/** What one sent message is known by afterwards. */\nexport interface OutreachSentMessage {\n /** Gmail's id for the message in the sender's mailbox. */\n gmailMessageId: string\n /** Gmail's thread id — what the next step of the sequence replies into. */\n threadId: string\n /** The RFC 5322 `Message-ID` it carried, for `In-Reply-To`/`References`. */\n messageId: string\n /**\n * The subject as sent — the thread's own subject when this message started\n * the thread, which a later step answers with `Re:`.\n */\n subject: string\n}\n\nexport interface SendOutreachMessageOptions {\n /** The Gmail thread to send into; a new thread when absent. */\n threadId?: string | null\n /** The `Date` header. Now when omitted. */\n date?: Date\n /** The Message-ID's domain. The From address's domain when omitted. */\n messageIdDomain?: string\n /**\n * The Message-ID's local part, minted by the caller so it knows the id\n * before the send answers (AGL-2981). A random one when omitted.\n */\n messageIdLocalPart?: string\n}\n\n/**\n * Builds a composed message and sends it through a mailbox's client\n * (AGL-2978). The one door a send goes through, so every Outreach message is\n * built by the same RFC 5322 writer and none can bypass its header checks.\n */\nexport async function sendOutreachMessage(\n client: Pick<GmailClient, 'sendMessage'>,\n message: OutreachComposedMessage,\n options: SendOutreachMessageOptions = {},\n): Promise<OutreachSentMessage> {\n const built = buildRfc5322Message(message, {\n date: options.date,\n messageIdDomain: options.messageIdDomain,\n messageIdLocalPart: options.messageIdLocalPart,\n })\n const sent = await client.sendMessage({\n raw: encodeGmailRawMessage(built.raw),\n threadId: options.threadId ?? null,\n })\n return {\n gmailMessageId: sent.id,\n threadId: sent.threadId,\n messageId: built.messageId,\n subject: built.subject,\n }\n}\n\n/**\n * Sends the email the engine composed (`composeOutreachEmail`) from a\n * mailbox, as the sending runtime does for a sequence step.\n *\n * The engine decides what is said and how it threads; this adds what the\n * engine leaves to the transport — the From line, from the mailbox's send-as\n * address and display name, and the MIME — and maps the rest onto headers:\n *\n * - a reply step goes into the engine's `threadId`, with its `In-Reply-To`\n * and `References`;\n * - the unsubscribe URL and mailto become one `List-Unsubscribe`, and the\n * URL adds `List-Unsubscribe-Post: List-Unsubscribe=One-Click` (RFC 8058).\n *\n * Answers the Gmail thread id, the `Message-ID` and the subject as sent,\n * which the runtime records on the enrollment for the next step to answer.\n */\nexport function sendComposedOutreachEmail(\n client: Pick<GmailClient, 'sendMessage'>,\n email: ComposedOutreachEmail,\n sender: OutreachMailAddress,\n options: Omit<SendOutreachMessageOptions, 'threadId'> = {},\n): Promise<OutreachSentMessage> {\n // The pair campaigns write too (AGL-3307): both URIs in one\n // `List-Unsubscribe`, and the one-click `Post` only beside the URL.\n const unsubscribe = listUnsubscribeHeaders({\n url: email.listUnsubscribeUrl,\n mailto: email.listUnsubscribeMailto,\n })\n return sendOutreachMessage(\n client,\n {\n from: sender,\n to: email.to,\n subject: email.subject,\n text: email.text,\n html: email.html ?? null,\n headers: {\n 'In-Reply-To': email.inReplyTo,\n References: email.references,\n 'List-Unsubscribe': unsubscribe['List-Unsubscribe'] ?? null,\n 'List-Unsubscribe-Post': unsubscribe['List-Unsubscribe-Post'] ?? null,\n },\n },\n { ...options, threadId: email.threadId ?? null },\n )\n}\n"],"names":["listUnsubscribeHeaders","buildRfc5322Message","encodeGmailRawMessage","sendOutreachMessage","client","message","options","built","date","messageIdDomain","messageIdLocalPart","sent","sendMessage","raw","threadId","gmailMessageId","id","messageId","subject","sendComposedOutreachEmail","email","sender","unsubscribe","url","listUnsubscribeUrl","mailto","listUnsubscribeMailto","from","to","text","html","headers","inReplyTo","References","references"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAID,SAASA,sBAAsB,QAAQ,4CAA2C;AAClF,SACEC,mBAAmB,EACnBC,qBAAqB,QAGhB,eAAW;AA+BlB;;;;CAIC,GACD,OAAO,eAAeC,oBACpBC,MAAwC,EACxCC,OAAgC,EAChCC,UAAsC,CAAC,CAAC;QAS5BA;IAPZ,MAAMC,QAAQN,oBAAoBI,SAAS;QACzCG,MAAMF,QAAQE,IAAI;QAClBC,iBAAiBH,QAAQG,eAAe;QACxCC,oBAAoBJ,QAAQI,kBAAkB;IAChD;IACA,MAAMC,OAAO,MAAMP,OAAOQ,WAAW,CAAC;QACpCC,KAAKX,sBAAsBK,MAAMM,GAAG;QACpCC,QAAQ,GAAER,oBAAAA,QAAQQ,QAAQ,YAAhBR,oBAAoB;IAChC;IACA,OAAO;QACLS,gBAAgBJ,KAAKK,EAAE;QACvBF,UAAUH,KAAKG,QAAQ;QACvBG,WAAWV,MAAMU,SAAS;QAC1BC,SAASX,MAAMW,OAAO;IACxB;AACF;AAEA;;;;;;;;;;;;;;;CAeC,GACD,OAAO,SAASC,0BACdf,MAAwC,EACxCgB,KAA4B,EAC5BC,MAA2B,EAC3Bf,UAAwD,CAAC,CAAC;QAehDc,aAIgBE,8BACKA,kCAGLF;IArB1B,4DAA4D;IAC5D,oEAAoE;IACpE,MAAME,cAActB,uBAAuB;QACzCuB,KAAKH,MAAMI,kBAAkB;QAC7BC,QAAQL,MAAMM,qBAAqB;IACrC;IACA,OAAOvB,oBACLC,QACA;QACEuB,MAAMN;QACNO,IAAIR,MAAMQ,EAAE;QACZV,SAASE,MAAMF,OAAO;QACtBW,MAAMT,MAAMS,IAAI;QAChBC,IAAI,GAAEV,cAAAA,MAAMU,IAAI,YAAVV,cAAc;QACpBW,SAAS;YACP,eAAeX,MAAMY,SAAS;YAC9BC,YAAYb,MAAMc,UAAU;YAC5B,kBAAkB,GAAEZ,+BAAAA,WAAW,CAAC,mBAAmB,YAA/BA,+BAAmC;YACvD,uBAAuB,GAAEA,mCAAAA,WAAW,CAAC,wBAAwB,YAApCA,mCAAwC;QACnE;IACF,GACA,aAAKhB;QAASQ,QAAQ,GAAEM,kBAAAA,MAAMN,QAAQ,YAAdM,kBAAkB;;AAE9C"}
|