@aglyn/plugins-outreach 1.0.0-beta.224 → 1.0.0-beta.225
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/README.md +10 -4
- package/package.json +14 -14
- package/src/lib/components/enrollment-detail.d.ts +8 -0
- package/src/lib/components/enrollment-detail.js +17 -5
- package/src/lib/components/enrollment-detail.js.map +1 -1
- package/src/lib/components/mailbox-card.d.ts +3 -3
- package/src/lib/components/mailbox-card.js +11 -5
- package/src/lib/components/mailbox-card.js.map +1 -1
- package/src/lib/components/mailboxes-section.d.ts +6 -5
- package/src/lib/components/mailboxes-section.js +61 -26
- package/src/lib/components/mailboxes-section.js.map +1 -1
- package/src/lib/components/sequence-detail.js +1 -1
- package/src/lib/components/sequence-detail.js.map +1 -1
- package/src/lib/components/sequence-editor.js +29 -4
- package/src/lib/components/sequence-editor.js.map +1 -1
- package/src/lib/components/sequence-mailbox-picker.d.ts +21 -0
- package/src/lib/components/sequence-mailbox-picker.js +74 -13
- package/src/lib/components/sequence-mailbox-picker.js.map +1 -1
- package/src/lib/components/use-outreach-mailbox-api.d.ts +10 -2
- package/src/lib/components/use-outreach-mailbox-api.js +7 -2
- package/src/lib/components/use-outreach-mailbox-api.js.map +1 -1
- package/src/lib/engine/enrollment-state.d.ts +6 -0
- package/src/lib/engine/enrollment-state.js +1 -1
- package/src/lib/engine/enrollment-state.js.map +1 -1
- package/src/lib/engine/index.d.ts +1 -0
- package/src/lib/engine/index.js +1 -0
- package/src/lib/engine/index.js.map +1 -1
- package/src/lib/engine/mailbox-notice.d.ts +5 -2
- package/src/lib/engine/mailbox-notice.js +6 -2
- package/src/lib/engine/mailbox-notice.js.map +1 -1
- package/src/lib/engine/mailbox-rotation.d.ts +41 -0
- package/src/lib/engine/mailbox-rotation.js +71 -0
- package/src/lib/engine/mailbox-rotation.js.map +1 -0
- package/src/lib/mailboxes/mailbox-api.d.ts +30 -11
- package/src/lib/mailboxes/mailbox-api.js +7 -3
- package/src/lib/mailboxes/mailbox-api.js.map +1 -1
- package/src/lib/mailboxes/mailbox-credentials.d.ts +15 -12
- package/src/lib/mailboxes/mailbox-credentials.js +12 -8
- package/src/lib/mailboxes/mailbox-credentials.js.map +1 -1
- package/src/lib/mailboxes/mailbox-erasure.js +3 -0
- package/src/lib/mailboxes/mailbox-erasure.js.map +1 -1
- package/src/lib/mailboxes/mailbox-revoke.d.ts +10 -5
- package/src/lib/mailboxes/mailbox-revoke.js +1 -0
- package/src/lib/mailboxes/mailbox-revoke.js.map +1 -1
- package/src/lib/mailboxes/mailbox-routes.d.ts +11 -3
- package/src/lib/mailboxes/mailbox-routes.js +218 -35
- package/src/lib/mailboxes/mailbox-routes.js.map +1 -1
- package/src/lib/mailboxes/mailbox-settings.d.ts +6 -2
- package/src/lib/mailboxes/mailbox-settings.js +5 -1
- package/src/lib/mailboxes/mailbox-settings.js.map +1 -1
- package/src/lib/mailboxes/mailbox-transport.d.ts +15 -9
- package/src/lib/mailboxes/mailbox-transport.js +47 -17
- package/src/lib/mailboxes/mailbox-transport.js.map +1 -1
- package/src/lib/mailboxes/oauth-state.d.ts +11 -3
- package/src/lib/mailboxes/oauth-state.js +17 -8
- package/src/lib/mailboxes/oauth-state.js.map +1 -1
- package/src/lib/mailboxes/outreach-config.d.ts +30 -0
- package/src/lib/mailboxes/outreach-config.js +53 -10
- package/src/lib/mailboxes/outreach-config.js.map +1 -1
- package/src/lib/mailboxes/register-mailbox-routes.js +2 -1
- package/src/lib/mailboxes/register-mailbox-routes.js.map +1 -1
- package/src/lib/model/outreach.types.d.ts +13 -2
- package/src/lib/model/outreach.types.js +7 -2
- package/src/lib/model/outreach.types.js.map +1 -1
- package/src/lib/model/sequence-draft.d.ts +6 -1
- package/src/lib/model/sequence-draft.js +16 -4
- package/src/lib/model/sequence-draft.js.map +1 -1
- package/src/lib/routes/enroll-routes.js +52 -1
- package/src/lib/routes/enroll-routes.js.map +1 -1
- package/src/lib/routes/sequence-routes.js +11 -3
- package/src/lib/routes/sequence-routes.js.map +1 -1
- package/src/lib/runtime/fixtures/fake-gmail.d.ts +18 -3
- package/src/lib/runtime/fixtures/fake-gmail.js +21 -1
- package/src/lib/runtime/fixtures/fake-gmail.js.map +1 -1
- package/src/lib/runtime/mailbox-notices.js +2 -2
- package/src/lib/runtime/mailbox-notices.js.map +1 -1
- package/src/lib/runtime/send-job.js +2 -8
- package/src/lib/runtime/send-job.js.map +1 -1
- package/src/lib/runtime/sync-job.js +24 -11
- package/src/lib/runtime/sync-job.js.map +1 -1
- package/src/lib/subprocessors.d.ts +12 -1
- package/src/lib/subprocessors.js +25 -2
- package/src/lib/subprocessors.js.map +1 -1
- package/src/lib/transfer/sequences-package.js +7 -2
- package/src/lib/transfer/sequences-package.js.map +1 -1
- package/src/lib/transport/gmail-client.d.ts +4 -11
- package/src/lib/transport/gmail-client.js +42 -17
- package/src/lib/transport/gmail-client.js.map +1 -1
- package/src/lib/transport/graph-client.d.ts +87 -0
- package/src/lib/transport/graph-client.js +359 -0
- package/src/lib/transport/graph-client.js.map +1 -0
- package/src/lib/transport/mail-client.d.ts +138 -0
- package/src/lib/transport/mail-client.js +34 -0
- package/src/lib/transport/mail-client.js.map +1 -0
- package/src/lib/transport/microsoft-errors.d.ts +32 -0
- package/src/lib/transport/microsoft-errors.js +94 -0
- package/src/lib/transport/microsoft-errors.js.map +1 -0
- package/src/lib/transport/microsoft-oauth.d.ts +140 -0
- package/src/lib/transport/microsoft-oauth.js +225 -0
- package/src/lib/transport/microsoft-oauth.js.map +1 -0
- package/src/lib/transport/mime-message.d.ts +30 -0
- package/src/lib/transport/mime-message.js +235 -0
- package/src/lib/transport/mime-message.js.map +1 -0
- package/src/lib/transport/send-message.d.ts +15 -8
- package/src/lib/transport/send-message.js +4 -3
- package/src/lib/transport/send-message.js.map +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/transport/graph-client.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 { GmailApiMessage } from '../engine/thread-message'\nimport { GmailTransportError } from './gmail-errors'\nimport { fetchWithBackoff, retryAfterMs, type TransportDeps } from './http'\nimport type {\n OutreachMailClient,\n OutreachMailHeader,\n OutreachMailSearch,\n OutreachMailStub,\n} from './mail-client'\nimport { graphApiError } from './microsoft-errors'\nimport { refreshMicrosoftAccessToken } from './microsoft-oauth'\nimport { gmailApiMessageFromMime } from './mime-message'\n\n/**\n * THE MICROSOFT GRAPH TRANSPORT (AGL-3489): one connected Microsoft 365\n * mailbox, behind the same seam as Gmail (`mail-client.ts`).\n *\n * ## Sending\n *\n * The message is the one the RFC 5322 writer built for Gmail, headers and\n * all — `List-Unsubscribe`, `In-Reply-To`, `References`, the `Message-ID`\n * the runtime minted. It is created as a draft from that MIME and the draft\n * is sent: creating it first is what answers the message's id and\n * `conversationId` (Graph's `sendMail` answers nothing), and the\n * conversation id is what a follow-up step and the sync know the thread by.\n * A follow-up threads by its own `In-Reply-To` and `References`, which is\n * how Exchange and every receiver group it, so `threadId` is not sent.\n *\n * Every call asks for immutable ids (`Prefer: IdType=\"ImmutableId\"`), so the\n * draft's id is still the message's id once sending has moved it to Sent\n * Items.\n *\n * ## Reading\n *\n * A message read whole is its MIME (`/$value`) read into the Gmail API\n * shape the classifier takes. A search lists the window's messages across\n * every folder (`/me/messages`, junk and deleted included) with the few\n * properties a search needs, and filters them here: Graph's `$filter` cannot\n * combine a received-time range with a sender list or a recipient, and the\n * window a sync pass reads is short.\n *\n * ## Tokens\n *\n * As the Gmail client: minted on first use, reused until a minute before\n * expiry, one refresh in flight, a 401 retried once with a fresh token.\n * Microsoft rotates refresh tokens, so a refresh that answers a new one\n * hands it to `onRefreshTokenRotated` for the caller to store, and uses it\n * from then on.\n */\n\nexport const GRAPH_API_BASE = 'https://graph.microsoft.com/v1.0'\nconst ME = `${GRAPH_API_BASE}/me`\n\nexport interface GraphClientOptions extends TransportDeps {\n clientId: string\n clientSecret: string\n tenant: string\n refreshToken: string\n /** The mailbox's own addresses: what a search's `notFromSelf` leaves out. */\n selfAddresses?: readonly string[]\n /** An access token already in hand — a connect's own — used until it expires. */\n accessToken?: { token: string; expiresInSeconds: number } | null\n /** Stores a refresh token Microsoft rotated in. Its failure is logged, not thrown. */\n onRefreshTokenRotated?: (refreshToken: string) => Promise<void> | void\n}\n\n/** `GET /me`: the account a grant is for. */\nexport interface GraphProfile {\n /** The directory object id — the ID token's `oid`. */\n id: string\n /** The mailbox's address: `mail`, or the sign-in name when it has none. */\n emailAddress: string\n displayName: string\n}\n\nexport interface GraphClient extends OutreachMailClient {\n readonly provider: 'microsoft'\n getProfile(): Promise<GraphProfile>\n revoke(): Promise<'unsupported'>\n}\n\n/** Refresh this long before the stated expiry. */\nconst EXPIRY_MARGIN_MS = 60_000\n/** Messages one search page lists; Graph allows up to 1000. */\nconst SEARCH_PAGE_SIZE = 250\n/** Messages of one conversation read whole. */\nconst THREAD_MAX = 50\n\nconst text = (value: unknown): string => (typeof value === 'string' ? value : '')\n\n/** An OData string literal: single quotes doubled. */\nconst literal = (value: string) => `'${value.replace(/'/g, \"''\")}'`\n\n/** A query string Graph reads: `%20`, never `+`, for a space. */\nfunction query(params: Record<string, string>): string {\n return Object.entries(params)\n .map(([key, value]) => `${key}=${encodeURIComponent(value)}`)\n .join('&')\n}\n\nconst addressOf = (recipient: unknown): string =>\n text((recipient as { emailAddress?: { address?: unknown } } | null)?.emailAddress?.address)\n .trim()\n .toLowerCase()\n\nconst addressesOf = (list: unknown): string[] => (Array.isArray(list) ? list.map(addressOf).filter(Boolean) : [])\n\nconst receivedMs = (record: Record<string, unknown>): number | null => {\n const at = Date.parse(text(record['receivedDateTime']))\n return Number.isFinite(at) ? at : null\n}\n\n/** Whether an address answers a `from` term: a whole address, or a local part. */\nfunction fromMatches(address: string, term: string): boolean {\n const wanted = term.trim().toLowerCase()\n if (!wanted || !address) return false\n if (wanted.includes('@')) return address === wanted\n return address.slice(0, address.indexOf('@')) === wanted\n}\n\n/** Whether a listed message answers a search. */\nexport function graphMessageMatches(\n record: Record<string, unknown>,\n search: OutreachMailSearch,\n self: ReadonlySet<string>,\n): boolean {\n const at = receivedMs(record)\n if (at !== null && at < search.afterMs) return false\n const from = addressOf(record['from'])\n if (search.notFromSelf && self.has(from)) return false\n if (search.from?.length && !search.from.some((term) => fromMatches(from, term))) return false\n if (search.to) {\n const to = search.to.trim().toLowerCase()\n const recipients = [...addressesOf(record['toRecipients']), ...addressesOf(record['ccRecipients'])]\n if (!recipients.includes(to)) return false\n }\n return true\n}\n\n/** The header list a metadata read answers, the named ones only. */\nfunction metadataHeaders(record: Record<string, unknown>, wanted: readonly string[] | undefined): OutreachMailHeader[] {\n const listed = (Array.isArray(record['internetMessageHeaders']) ? record['internetMessageHeaders'] : [])\n .map((entry) => entry as { name?: unknown; value?: unknown })\n .filter((entry) => typeof entry.name === 'string')\n .map((entry) => ({ name: String(entry.name), value: text(entry.value) }))\n const has = (name: string) => listed.some((header) => header.name.toLowerCase() === name.toLowerCase())\n const from = record['from'] as { emailAddress?: { name?: unknown; address?: unknown } } | undefined\n const fromName = text(from?.emailAddress?.name)\n const fromAddress = text(from?.emailAddress?.address)\n // A sent message carries no internet headers in Graph; its properties say the same.\n const synthesized: OutreachMailHeader[] = [\n { name: 'Subject', value: text(record['subject']) },\n { name: 'From', value: fromName && fromAddress ? `${fromName} <${fromAddress}>` : fromAddress },\n { name: 'To', value: addressesOf(record['toRecipients']).join(', ') },\n { name: 'Message-ID', value: text(record['internetMessageId']) },\n ].filter((header) => header.value && !has(header.name))\n const all = [...listed, ...synthesized]\n if (!wanted?.length) return all\n const names = new Set(wanted.map((name) => name.toLowerCase()))\n return all.filter((header) => names.has(header.name.toLowerCase()))\n}\n\n/** A Graph client for one mailbox. See the module comment. */\nexport function createGraphClient(options: GraphClientOptions): GraphClient {\n const deps: TransportDeps = options\n const now = options.now ?? Date.now\n const self = new Set((options.selfAddresses ?? []).map((address) => address.trim().toLowerCase()).filter(Boolean))\n let refreshToken = options.refreshToken\n let cached: { token: string; expiresAtMs: number } | null = options.accessToken?.token\n ? { token: options.accessToken.token, expiresAtMs: now() + options.accessToken.expiresInSeconds * 1000 }\n : null\n let refreshing: Promise<string> | null = null\n\n const mint = (): Promise<string> => {\n refreshing ??= refreshMicrosoftAccessToken(\n { clientId: options.clientId, clientSecret: options.clientSecret, tenant: options.tenant, refreshToken },\n deps,\n )\n .then(async (minted) => {\n cached = { token: minted.accessToken, expiresAtMs: now() + minted.expiresInSeconds * 1000 }\n if (minted.refreshToken && minted.refreshToken !== refreshToken) {\n refreshToken = minted.refreshToken\n try {\n await options.onRefreshTokenRotated?.(minted.refreshToken)\n } catch (error) {\n console.error('[outreach] storing a rotated Microsoft refresh token failed', error)\n }\n }\n return minted.accessToken\n })\n .finally(() => {\n refreshing = null\n })\n return refreshing\n }\n\n const getAccessToken = async (): Promise<string> => {\n if (cached && cached.expiresAtMs - EXPIRY_MARGIN_MS > now()) return cached.token\n return mint()\n }\n\n /**\n * One Graph call: authorized, retried per policy, a 401 once refreshed.\n * Answers the response, already judged ok.\n */\n const send = async (url: string, init: RequestInit = {}): Promise<Response> => {\n for (let tries = 0; ; tries += 1) {\n const token = await getAccessToken()\n const response = await fetchWithBackoff(\n url,\n {\n ...init,\n headers: {\n Accept: 'application/json',\n Prefer: 'IdType=\"ImmutableId\"',\n ...(init.headers as Record<string, string> | undefined),\n Authorization: `Bearer ${token}`,\n },\n },\n deps,\n )\n if (response.status === 401 && tries === 0) {\n cached = null\n continue\n }\n if (response.ok) return response\n const body = await response.json().catch(() => null)\n throw graphApiError(response.status, body, { retryAfterMs: retryAfterMs(response, now()) })\n }\n }\n\n const json = async <T>(url: string, init: RequestInit = {}): Promise<T> => {\n const response = await send(url, init)\n const body = await response.json().catch(() => null)\n if (body === null || typeof body !== 'object') {\n throw new GmailTransportError('unexpected', 'Microsoft Graph answered with a body that is not JSON.', {\n status: response.status,\n })\n }\n return body as T\n }\n\n const messagePath = (id: string) => {\n if (!id) throw new GmailTransportError('invalid_request', 'A message to read needs its id.')\n return `${ME}/messages/${encodeURIComponent(id)}`\n }\n\n const getFullMessage = async (messageId: string): Promise<GmailApiMessage> => {\n const path = messagePath(messageId)\n const [record, raw] = await Promise.all([\n json<Record<string, unknown>>(`${path}?${query({ $select: 'id,conversationId,receivedDateTime' })}`),\n send(`${path}/$value`, { headers: { Accept: 'message/rfc822, text/plain, */*' } }).then((response) =>\n response.arrayBuffer(),\n ),\n ])\n return gmailApiMessageFromMime(new Uint8Array(raw), {\n id: text(record['id']) || messageId,\n threadId: text(record['conversationId']),\n internalDateMs: receivedMs(record),\n })\n }\n\n return {\n provider: 'microsoft',\n getAccessToken,\n\n async getProfile() {\n const body = await json<Record<string, unknown>>(`${ME}?${query({ $select: 'id,mail,userPrincipalName,displayName' })}`)\n return {\n id: text(body['id']).toLowerCase(),\n emailAddress: (text(body['mail']) || text(body['userPrincipalName'])).trim().toLowerCase(),\n displayName: text(body['displayName']).trim(),\n }\n },\n\n async sendMessage(input) {\n if (!input?.raw) {\n throw new GmailTransportError('invalid_request', 'A message to send needs its raw content.')\n }\n // Graph takes MIME as standard base64 in a text/plain body.\n const mime = Buffer.from(input.raw, 'base64url').toString('base64')\n const draft = await json<Record<string, unknown>>(`${ME}/messages`, {\n method: 'POST',\n headers: { 'Content-Type': 'text/plain' },\n body: mime,\n })\n const id = text(draft['id'])\n if (!id) throw new GmailTransportError('unexpected', 'Microsoft Graph created the message without an id.')\n try {\n await send(`${messagePath(id)}/send`, { method: 'POST' })\n } catch (error) {\n // The draft would otherwise sit in Drafts, unsent, looking like work.\n await send(messagePath(id), { method: 'DELETE' }).catch(() => undefined)\n throw error\n }\n return {\n id,\n threadId: text(draft['conversationId']),\n internetMessageId: text(draft['internetMessageId']) || null,\n }\n },\n\n async getFullThread(threadId) {\n const list = await json<{ value?: unknown }>(\n `${ME}/messages?${query({\n $filter: `conversationId eq ${literal(threadId)}`,\n $select: 'id,conversationId,receivedDateTime',\n $top: String(THREAD_MAX),\n })}`,\n )\n const records = (Array.isArray(list.value) ? list.value : [])\n .map((entry) => (entry ?? {}) as Record<string, unknown>)\n .filter((record) => text(record['id']))\n .sort((a, b) => (receivedMs(a) ?? 0) - (receivedMs(b) ?? 0))\n const messages: GmailApiMessage[] = []\n for (const record of records) messages.push(await getFullMessage(text(record['id'])))\n return { id: threadId, historyId: null, messages }\n },\n\n getFullMessage,\n\n async getMessage(messageId, messageOptions) {\n const record = await json<Record<string, unknown>>(\n `${messagePath(messageId)}?${query({\n $select:\n 'id,conversationId,subject,from,toRecipients,receivedDateTime,bodyPreview,internetMessageId,internetMessageHeaders',\n })}`,\n )\n return {\n id: text(record['id']) || messageId,\n threadId: text(record['conversationId']),\n labelIds: [],\n snippet: text(record['bodyPreview']),\n internalDateMs: receivedMs(record),\n headers: metadataHeaders(record, messageOptions?.metadataHeaders),\n }\n },\n\n async searchMessages(search, page) {\n let url: string\n if (page?.pageToken) {\n // A next link is Graph's own address; anything else is not followed\n // with this mailbox's token.\n if (!page.pageToken.startsWith(`${ME}/messages?`)) {\n throw new GmailTransportError('invalid_request', 'That page token is not a Microsoft Graph page.')\n }\n url = page.pageToken\n } else {\n url = `${ME}/messages?${query({\n $filter: `receivedDateTime ge ${new Date(Math.max(0, search.afterMs)).toISOString()}`,\n $select: 'id,conversationId,from,toRecipients,ccRecipients,receivedDateTime',\n $top: String(Math.max(page?.maxResults ?? 0, SEARCH_PAGE_SIZE)),\n })}`\n }\n const body = await json<Record<string, unknown>>(url)\n const messages: OutreachMailStub[] = (Array.isArray(body['value']) ? body['value'] : [])\n .map((entry) => (entry ?? {}) as Record<string, unknown>)\n .filter((record) => text(record['id']) && graphMessageMatches(record, search, self))\n .map((record) => ({ id: text(record['id']), threadId: text(record['conversationId']) }))\n return { messages, nextPageToken: text(body['@odata.nextLink']) || null }\n },\n\n async findMessageByMessageId(messageId) {\n const wanted = String(messageId ?? '').trim()\n if (!wanted) return null\n const id = wanted.startsWith('<') ? wanted : `<${wanted}>`\n const body = await json<{ value?: unknown }>(\n `${ME}/messages?${query({\n $filter: `internetMessageId eq ${literal(id)}`,\n $select: 'id,conversationId',\n $top: '1',\n })}`,\n )\n const record = (Array.isArray(body.value) ? body.value[0] : null) as Record<string, unknown> | null\n return record && text(record['id']) ? { id: text(record['id']), threadId: text(record['conversationId']) } : null\n },\n\n async revoke() {\n return 'unsupported' as const\n },\n }\n}\n"],"names":["GmailTransportError","fetchWithBackoff","retryAfterMs","graphApiError","refreshMicrosoftAccessToken","gmailApiMessageFromMime","GRAPH_API_BASE","ME","EXPIRY_MARGIN_MS","SEARCH_PAGE_SIZE","THREAD_MAX","text","value","literal","replace","query","params","Object","entries","map","key","encodeURIComponent","join","addressOf","recipient","emailAddress","address","trim","toLowerCase","addressesOf","list","Array","isArray","filter","Boolean","receivedMs","record","at","Date","parse","Number","isFinite","fromMatches","term","wanted","includes","slice","indexOf","graphMessageMatches","search","self","afterMs","from","notFromSelf","has","length","some","to","recipients","metadataHeaders","listed","entry","name","String","header","fromName","fromAddress","synthesized","all","names","Set","createGraphClient","options","deps","now","selfAddresses","refreshToken","cached","accessToken","token","expiresAtMs","expiresInSeconds","refreshing","mint","clientId","clientSecret","tenant","then","minted","onRefreshTokenRotated","error","console","finally","getAccessToken","send","url","init","tries","response","headers","Accept","Prefer","Authorization","status","ok","body","json","catch","messagePath","id","getFullMessage","messageId","path","raw","Promise","$select","arrayBuffer","Uint8Array","threadId","internalDateMs","provider","getProfile","displayName","sendMessage","input","mime","Buffer","toString","draft","method","undefined","internetMessageId","getFullThread","$filter","$top","records","sort","a","b","messages","push","historyId","getMessage","messageOptions","labelIds","snippet","searchMessages","page","pageToken","startsWith","Math","max","toISOString","maxResults","nextPageToken","findMessageByMessageId","revoke"],"mappings":"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"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import type { GmailApiMessage } from '../engine/thread-message';
|
|
18
|
+
import type { OutreachMailboxProvider } from '../model/outreach.types';
|
|
19
|
+
/**
|
|
20
|
+
* THE PROVIDER SEAM (AGL-3489): what the sending runtime, the sync and the
|
|
21
|
+
* panel's test send ask of a connected mailbox, whichever provider runs it.
|
|
22
|
+
*
|
|
23
|
+
* Google's Gmail REST API (`gmail-client.ts`) and Microsoft Graph
|
|
24
|
+
* (`graph-client.ts`) each implement it. Two things keep the engine
|
|
25
|
+
* provider-blind:
|
|
26
|
+
*
|
|
27
|
+
* - A message read whole is handed over as a Gmail API `format=full`
|
|
28
|
+
* resource. Gmail returns that shape itself; the Graph client reads a
|
|
29
|
+
* message's MIME and builds the same tree (`mime-message.ts`), so the
|
|
30
|
+
* classifier reads a reply, an automatic answer or a delivery report the
|
|
31
|
+
* same way from either.
|
|
32
|
+
* - A search is a structured {@link OutreachMailSearch}, not a provider's
|
|
33
|
+
* query language. Gmail turns it into a `q` ({@link gmailSearchQuery});
|
|
34
|
+
* Graph lists the window and filters it.
|
|
35
|
+
*
|
|
36
|
+
* A "thread" is the provider's conversation: Gmail's thread id, or Graph's
|
|
37
|
+
* `conversationId`. Every failure is a `GmailTransportError`, the transport's
|
|
38
|
+
* one error type, whose `reconnectRequired` and `retryable` mean the same for
|
|
39
|
+
* either provider.
|
|
40
|
+
*/
|
|
41
|
+
/** The messages a sync pass looks for, received after `afterMs`. */
|
|
42
|
+
export interface OutreachMailSearch {
|
|
43
|
+
/** Received at or after this moment, epoch ms. */
|
|
44
|
+
afterMs: number;
|
|
45
|
+
/** Leave out what the mailbox sent itself. */
|
|
46
|
+
notFromSelf?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* From any of these. An entry with an `@` is a whole address; one without
|
|
49
|
+
* names a sender's local part, as `mailer-daemon` names a bounce's.
|
|
50
|
+
*/
|
|
51
|
+
from?: readonly string[];
|
|
52
|
+
/** Addressed to this address. */
|
|
53
|
+
to?: string;
|
|
54
|
+
}
|
|
55
|
+
/** A message a search found: its id and the conversation it is in. */
|
|
56
|
+
export interface OutreachMailStub {
|
|
57
|
+
id: string;
|
|
58
|
+
threadId: string;
|
|
59
|
+
}
|
|
60
|
+
export interface OutreachMailSearchPage {
|
|
61
|
+
messages: OutreachMailStub[];
|
|
62
|
+
nextPageToken: string | null;
|
|
63
|
+
}
|
|
64
|
+
/** One header of a message read without its body. */
|
|
65
|
+
export interface OutreachMailHeader {
|
|
66
|
+
name: string;
|
|
67
|
+
value: string;
|
|
68
|
+
}
|
|
69
|
+
/** A message read for its headers alone. */
|
|
70
|
+
export interface OutreachMailMetadata {
|
|
71
|
+
id: string;
|
|
72
|
+
threadId: string;
|
|
73
|
+
labelIds: string[];
|
|
74
|
+
snippet: string;
|
|
75
|
+
internalDateMs: number | null;
|
|
76
|
+
headers: OutreachMailHeader[];
|
|
77
|
+
}
|
|
78
|
+
/** A conversation, every message read whole. */
|
|
79
|
+
export interface OutreachMailFullThread {
|
|
80
|
+
id: string;
|
|
81
|
+
historyId: string | null;
|
|
82
|
+
messages: GmailApiMessage[];
|
|
83
|
+
}
|
|
84
|
+
/** What a send answers. */
|
|
85
|
+
export interface OutreachMailSent {
|
|
86
|
+
/** The provider's id for the sent message. */
|
|
87
|
+
id: string;
|
|
88
|
+
/** The conversation it went into. */
|
|
89
|
+
threadId: string;
|
|
90
|
+
/**
|
|
91
|
+
* The `Message-ID` the provider sent it under, when it says. A provider
|
|
92
|
+
* that kept the one the message was written with answers that one.
|
|
93
|
+
*/
|
|
94
|
+
internetMessageId?: string | null;
|
|
95
|
+
labelIds?: string[];
|
|
96
|
+
}
|
|
97
|
+
/** What became of a grant at the provider when it was asked to end it. */
|
|
98
|
+
export type OutreachMailRevocation = 'revoked' | 'already-invalid' | 'unsupported';
|
|
99
|
+
export interface OutreachMailClient {
|
|
100
|
+
readonly provider: OutreachMailboxProvider;
|
|
101
|
+
/** A live access token, minted or reused. */
|
|
102
|
+
getAccessToken(): Promise<string>;
|
|
103
|
+
/**
|
|
104
|
+
* Sends a base64url RFC 5322 message (`encodeGmailRawMessage`). `threadId`
|
|
105
|
+
* files a follow-up into the conversation its first step started where
|
|
106
|
+
* the provider files by id; a provider that threads by the message's own
|
|
107
|
+
* `In-Reply-To` and `References` ignores it.
|
|
108
|
+
*/
|
|
109
|
+
sendMessage(input: {
|
|
110
|
+
raw: string;
|
|
111
|
+
threadId?: string | null;
|
|
112
|
+
}): Promise<OutreachMailSent>;
|
|
113
|
+
/** Every message of a conversation, whole, for the classifier. */
|
|
114
|
+
getFullThread(threadId: string): Promise<OutreachMailFullThread>;
|
|
115
|
+
/** One message, whole. */
|
|
116
|
+
getFullMessage(messageId: string): Promise<GmailApiMessage>;
|
|
117
|
+
/** One message's headers, only the ones named. */
|
|
118
|
+
getMessage(messageId: string, options?: {
|
|
119
|
+
metadataHeaders?: readonly string[];
|
|
120
|
+
}): Promise<OutreachMailMetadata>;
|
|
121
|
+
/** One page of the messages a search finds, spam and deleted mail included. */
|
|
122
|
+
searchMessages(search: OutreachMailSearch, page?: {
|
|
123
|
+
pageToken?: string | null;
|
|
124
|
+
maxResults?: number;
|
|
125
|
+
}): Promise<OutreachMailSearchPage>;
|
|
126
|
+
/** The message this mailbox holds under a `Message-ID`, or `null`. */
|
|
127
|
+
findMessageByMessageId(messageId: string): Promise<OutreachMailStub | null>;
|
|
128
|
+
/** Ends the grant at the provider, where the provider has a way to. */
|
|
129
|
+
revoke(): Promise<OutreachMailRevocation>;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* A structured search as a Gmail `q`. `after:` takes whole seconds, and a
|
|
133
|
+
* `from:` list is always parenthesized, so one address and several read the
|
|
134
|
+
* same.
|
|
135
|
+
*/
|
|
136
|
+
export declare function gmailSearchQuery(search: OutreachMailSearch): string;
|
|
137
|
+
/** A `Message-ID` without its angle brackets, as Gmail's `rfc822msgid:` takes it. */
|
|
138
|
+
export declare function bareMessageId(messageId: string): string;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /**
|
|
17
|
+
* A structured search as a Gmail `q`. `after:` takes whole seconds, and a
|
|
18
|
+
* `from:` list is always parenthesized, so one address and several read the
|
|
19
|
+
* same.
|
|
20
|
+
*/ export function gmailSearchQuery(search) {
|
|
21
|
+
var _search_from;
|
|
22
|
+
const terms = [
|
|
23
|
+
`after:${Math.floor(search.afterMs / 1000)}`
|
|
24
|
+
];
|
|
25
|
+
if (search.notFromSelf) terms.push('-from:me');
|
|
26
|
+
if ((_search_from = search.from) == null ? void 0 : _search_from.length) terms.push(`from:(${search.from.join(' OR ')})`);
|
|
27
|
+
if (search.to) terms.push(`to:${search.to}`);
|
|
28
|
+
return terms.join(' ');
|
|
29
|
+
}
|
|
30
|
+
/** A `Message-ID` without its angle brackets, as Gmail's `rfc822msgid:` takes it. */ export function bareMessageId(messageId) {
|
|
31
|
+
return String(messageId != null ? messageId : '').trim().replace(/^<|>$/g, '');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
//# sourceMappingURL=mail-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/transport/mail-client.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 { GmailApiMessage } from '../engine/thread-message'\nimport type { OutreachMailboxProvider } from '../model/outreach.types'\n\n/**\n * THE PROVIDER SEAM (AGL-3489): what the sending runtime, the sync and the\n * panel's test send ask of a connected mailbox, whichever provider runs it.\n *\n * Google's Gmail REST API (`gmail-client.ts`) and Microsoft Graph\n * (`graph-client.ts`) each implement it. Two things keep the engine\n * provider-blind:\n *\n * - A message read whole is handed over as a Gmail API `format=full`\n * resource. Gmail returns that shape itself; the Graph client reads a\n * message's MIME and builds the same tree (`mime-message.ts`), so the\n * classifier reads a reply, an automatic answer or a delivery report the\n * same way from either.\n * - A search is a structured {@link OutreachMailSearch}, not a provider's\n * query language. Gmail turns it into a `q` ({@link gmailSearchQuery});\n * Graph lists the window and filters it.\n *\n * A \"thread\" is the provider's conversation: Gmail's thread id, or Graph's\n * `conversationId`. Every failure is a `GmailTransportError`, the transport's\n * one error type, whose `reconnectRequired` and `retryable` mean the same for\n * either provider.\n */\n\n/** The messages a sync pass looks for, received after `afterMs`. */\nexport interface OutreachMailSearch {\n /** Received at or after this moment, epoch ms. */\n afterMs: number\n /** Leave out what the mailbox sent itself. */\n notFromSelf?: boolean\n /**\n * From any of these. An entry with an `@` is a whole address; one without\n * names a sender's local part, as `mailer-daemon` names a bounce's.\n */\n from?: readonly string[]\n /** Addressed to this address. */\n to?: string\n}\n\n/** A message a search found: its id and the conversation it is in. */\nexport interface OutreachMailStub {\n id: string\n threadId: string\n}\n\nexport interface OutreachMailSearchPage {\n messages: OutreachMailStub[]\n nextPageToken: string | null\n}\n\n/** One header of a message read without its body. */\nexport interface OutreachMailHeader {\n name: string\n value: string\n}\n\n/** A message read for its headers alone. */\nexport interface OutreachMailMetadata {\n id: string\n threadId: string\n labelIds: string[]\n snippet: string\n internalDateMs: number | null\n headers: OutreachMailHeader[]\n}\n\n/** A conversation, every message read whole. */\nexport interface OutreachMailFullThread {\n id: string\n historyId: string | null\n messages: GmailApiMessage[]\n}\n\n/** What a send answers. */\nexport interface OutreachMailSent {\n /** The provider's id for the sent message. */\n id: string\n /** The conversation it went into. */\n threadId: string\n /**\n * The `Message-ID` the provider sent it under, when it says. A provider\n * that kept the one the message was written with answers that one.\n */\n internetMessageId?: string | null\n labelIds?: string[]\n}\n\n/** What became of a grant at the provider when it was asked to end it. */\nexport type OutreachMailRevocation = 'revoked' | 'already-invalid' | 'unsupported'\n\nexport interface OutreachMailClient {\n readonly provider: OutreachMailboxProvider\n /** A live access token, minted or reused. */\n getAccessToken(): Promise<string>\n /**\n * Sends a base64url RFC 5322 message (`encodeGmailRawMessage`). `threadId`\n * files a follow-up into the conversation its first step started where\n * the provider files by id; a provider that threads by the message's own\n * `In-Reply-To` and `References` ignores it.\n */\n sendMessage(input: { raw: string; threadId?: string | null }): Promise<OutreachMailSent>\n /** Every message of a conversation, whole, for the classifier. */\n getFullThread(threadId: string): Promise<OutreachMailFullThread>\n /** One message, whole. */\n getFullMessage(messageId: string): Promise<GmailApiMessage>\n /** One message's headers, only the ones named. */\n getMessage(messageId: string, options?: { metadataHeaders?: readonly string[] }): Promise<OutreachMailMetadata>\n /** One page of the messages a search finds, spam and deleted mail included. */\n searchMessages(\n search: OutreachMailSearch,\n page?: { pageToken?: string | null; maxResults?: number },\n ): Promise<OutreachMailSearchPage>\n /** The message this mailbox holds under a `Message-ID`, or `null`. */\n findMessageByMessageId(messageId: string): Promise<OutreachMailStub | null>\n /** Ends the grant at the provider, where the provider has a way to. */\n revoke(): Promise<OutreachMailRevocation>\n}\n\n/**\n * A structured search as a Gmail `q`. `after:` takes whole seconds, and a\n * `from:` list is always parenthesized, so one address and several read the\n * same.\n */\nexport function gmailSearchQuery(search: OutreachMailSearch): string {\n const terms = [`after:${Math.floor(search.afterMs / 1000)}`]\n if (search.notFromSelf) terms.push('-from:me')\n if (search.from?.length) terms.push(`from:(${search.from.join(' OR ')})`)\n if (search.to) terms.push(`to:${search.to}`)\n return terms.join(' ')\n}\n\n/** A `Message-ID` without its angle brackets, as Gmail's `rfc822msgid:` takes it. */\nexport function bareMessageId(messageId: string): string {\n return String(messageId ?? '').trim().replace(/^<|>$/g, '')\n}\n"],"names":["gmailSearchQuery","search","terms","Math","floor","afterMs","notFromSelf","push","from","length","join","to","bareMessageId","messageId","String","trim","replace"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GA0HD;;;;CAIC,GACD,OAAO,SAASA,iBAAiBC,MAA0B;QAGrDA;IAFJ,MAAMC,QAAQ;QAAC,CAAC,MAAM,EAAEC,KAAKC,KAAK,CAACH,OAAOI,OAAO,GAAG,OAAO;KAAC;IAC5D,IAAIJ,OAAOK,WAAW,EAAEJ,MAAMK,IAAI,CAAC;IACnC,KAAIN,eAAAA,OAAOO,IAAI,qBAAXP,aAAaQ,MAAM,EAAEP,MAAMK,IAAI,CAAC,CAAC,MAAM,EAAEN,OAAOO,IAAI,CAACE,IAAI,CAAC,QAAQ,CAAC,CAAC;IACxE,IAAIT,OAAOU,EAAE,EAAET,MAAMK,IAAI,CAAC,CAAC,GAAG,EAAEN,OAAOU,EAAE,EAAE;IAC3C,OAAOT,MAAMQ,IAAI,CAAC;AACpB;AAEA,mFAAmF,GACnF,OAAO,SAASE,cAAcC,SAAiB;IAC7C,OAAOC,OAAOD,oBAAAA,YAAa,IAAIE,IAAI,GAAGC,OAAO,CAAC,UAAU;AAC1D"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { GmailTransportError } from './gmail-errors';
|
|
18
|
+
/**
|
|
19
|
+
* WHAT A MICROSOFT CALL CAN FAIL WITH (AGL-3489), mapped onto the
|
|
20
|
+
* transport's one error type so a caller decides the same three things it
|
|
21
|
+
* decides for Google: reconnect, retry later, or give up.
|
|
22
|
+
*
|
|
23
|
+
* Microsoft answers in two dialects as well: the identity platform's token
|
|
24
|
+
* endpoint (`{ error: 'invalid_grant', error_description: 'AADSTS…' }`) and
|
|
25
|
+
* Graph (`{ error: { code: 'ErrorAccessDenied', message } }`).
|
|
26
|
+
*/
|
|
27
|
+
/** The token endpoint's error body, mapped. */
|
|
28
|
+
export declare function microsoftTokenEndpointError(status: number, body: unknown): GmailTransportError;
|
|
29
|
+
/** A Graph error response, mapped. */
|
|
30
|
+
export declare function graphApiError(status: number, body: unknown, init?: {
|
|
31
|
+
retryAfterMs?: number | null;
|
|
32
|
+
}): GmailTransportError;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { GmailTransportError } from "./gmail-errors.js";
|
|
17
|
+
/**
|
|
18
|
+
* WHAT A MICROSOFT CALL CAN FAIL WITH (AGL-3489), mapped onto the
|
|
19
|
+
* transport's one error type so a caller decides the same three things it
|
|
20
|
+
* decides for Google: reconnect, retry later, or give up.
|
|
21
|
+
*
|
|
22
|
+
* Microsoft answers in two dialects as well: the identity platform's token
|
|
23
|
+
* endpoint (`{ error: 'invalid_grant', error_description: 'AADSTS…' }`) and
|
|
24
|
+
* Graph (`{ error: { code: 'ErrorAccessDenied', message } }`).
|
|
25
|
+
*/ /** The token endpoint's error body, mapped. */ export function microsoftTokenEndpointError(status, body) {
|
|
26
|
+
var _ref;
|
|
27
|
+
var _exec;
|
|
28
|
+
const record = body != null ? body : {};
|
|
29
|
+
const reason = typeof record.error === 'string' ? record.error : null;
|
|
30
|
+
const described = typeof record.error_description === 'string' ? record.error_description : '';
|
|
31
|
+
// The AADSTS code alone: the description goes on to name the tenant and the user.
|
|
32
|
+
const aadsts = (_ref = (_exec = /AADSTS\d+/.exec(described)) == null ? void 0 : _exec[0]) != null ? _ref : null;
|
|
33
|
+
const init = {
|
|
34
|
+
status,
|
|
35
|
+
providerReason: aadsts != null ? aadsts : reason
|
|
36
|
+
};
|
|
37
|
+
if (reason === 'invalid_grant' || reason === 'interaction_required') {
|
|
38
|
+
return new GmailTransportError('invalid_grant', 'Microsoft refused the mailbox grant. The mailbox must be connected again.', init);
|
|
39
|
+
}
|
|
40
|
+
if (reason === 'invalid_client' || reason === 'unauthorized_client') {
|
|
41
|
+
return new GmailTransportError('client_misconfigured', 'Microsoft refused this deployment’s app registration. Check its client id and secret.', init);
|
|
42
|
+
}
|
|
43
|
+
if (reason === 'invalid_scope' || reason === 'consent_required') {
|
|
44
|
+
return new GmailTransportError('insufficient_scope', 'Microsoft refused a requested permission.', init);
|
|
45
|
+
}
|
|
46
|
+
return new GmailTransportError(status >= 500 ? 'unavailable' : 'invalid_request', `Microsoft’s token endpoint refused the request${aadsts ? ` (${aadsts})` : '.'}`, init);
|
|
47
|
+
}
|
|
48
|
+
const QUOTA = new Set([
|
|
49
|
+
'ErrorQuotaExceeded',
|
|
50
|
+
'ErrorSubmissionQuotaExceeded',
|
|
51
|
+
'ErrorExceededMessageLimit'
|
|
52
|
+
]);
|
|
53
|
+
const SCOPE = new Set([
|
|
54
|
+
'ErrorAccessDenied',
|
|
55
|
+
'AccessDenied',
|
|
56
|
+
'Authorization_RequestDenied'
|
|
57
|
+
]);
|
|
58
|
+
const NO_MAILBOX = new Set([
|
|
59
|
+
'MailboxNotEnabledForRESTAPI',
|
|
60
|
+
'MailboxNotHostedInExchangeOnline',
|
|
61
|
+
'ErrorMailboxNotEnabledForRESTAPI'
|
|
62
|
+
]);
|
|
63
|
+
const NOT_FOUND = new Set([
|
|
64
|
+
'ErrorItemNotFound',
|
|
65
|
+
'ErrorInvalidIdMalformed',
|
|
66
|
+
'ResourceNotFound'
|
|
67
|
+
]);
|
|
68
|
+
/** A Graph error response, mapped. */ export function graphApiError(status, body, init = {}) {
|
|
69
|
+
const error = body == null ? void 0 : body.error;
|
|
70
|
+
const code = typeof (error == null ? void 0 : error.code) === 'string' ? error.code : null;
|
|
71
|
+
const message = typeof (error == null ? void 0 : error.message) === 'string' ? error.message : '';
|
|
72
|
+
const detail = message ? `: ${message.slice(0, 200)}` : '.';
|
|
73
|
+
const make = (transportCode, text)=>new GmailTransportError(transportCode, text, {
|
|
74
|
+
status,
|
|
75
|
+
providerReason: code,
|
|
76
|
+
retryAfterMs: init.retryAfterMs
|
|
77
|
+
});
|
|
78
|
+
if (status === 429) return make('rate_limited', 'Microsoft is rate-limiting this mailbox. Try again later.');
|
|
79
|
+
if (code && QUOTA.has(code)) return make('quota_exceeded', 'This mailbox has spent its Microsoft sending limit.');
|
|
80
|
+
if (code && NO_MAILBOX.has(code)) {
|
|
81
|
+
return make('mail_service_unavailable', 'This Microsoft account has no Exchange Online mailbox.');
|
|
82
|
+
}
|
|
83
|
+
if (status >= 500) return make('unavailable', `Microsoft Graph is unavailable${detail}`);
|
|
84
|
+
if (status === 401) return make('unauthorized', 'Microsoft Graph refused the mailbox’s access token.');
|
|
85
|
+
if (status === 403 && code && SCOPE.has(code)) {
|
|
86
|
+
return make('insufficient_scope', 'The mailbox grant is missing a permission Sequences needs.');
|
|
87
|
+
}
|
|
88
|
+
if (status === 404 || code && NOT_FOUND.has(code)) return make('not_found', `Microsoft Graph found nothing there${detail}`);
|
|
89
|
+
if (status === 400) return make('invalid_request', `Microsoft Graph refused the request${detail}`);
|
|
90
|
+
if (status === 403) return make('forbidden', `Microsoft Graph refused the request${detail}`);
|
|
91
|
+
return make('unexpected', `Microsoft Graph answered ${status}${detail}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
//# sourceMappingURL=microsoft-errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/transport/microsoft-errors.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 { GmailTransportError, type GmailTransportErrorCode } from './gmail-errors'\n\n/**\n * WHAT A MICROSOFT CALL CAN FAIL WITH (AGL-3489), mapped onto the\n * transport's one error type so a caller decides the same three things it\n * decides for Google: reconnect, retry later, or give up.\n *\n * Microsoft answers in two dialects as well: the identity platform's token\n * endpoint (`{ error: 'invalid_grant', error_description: 'AADSTS…' }`) and\n * Graph (`{ error: { code: 'ErrorAccessDenied', message } }`).\n */\n\n/** The token endpoint's error body, mapped. */\nexport function microsoftTokenEndpointError(status: number, body: unknown): GmailTransportError {\n const record = (body ?? {}) as { error?: unknown; error_description?: unknown }\n const reason = typeof record.error === 'string' ? record.error : null\n const described = typeof record.error_description === 'string' ? record.error_description : ''\n // The AADSTS code alone: the description goes on to name the tenant and the user.\n const aadsts = /AADSTS\\d+/.exec(described)?.[0] ?? null\n const init = { status, providerReason: aadsts ?? reason }\n if (reason === 'invalid_grant' || reason === 'interaction_required') {\n return new GmailTransportError(\n 'invalid_grant',\n 'Microsoft refused the mailbox grant. The mailbox must be connected again.',\n init,\n )\n }\n if (reason === 'invalid_client' || reason === 'unauthorized_client') {\n return new GmailTransportError(\n 'client_misconfigured',\n 'Microsoft refused this deployment’s app registration. Check its client id and secret.',\n init,\n )\n }\n if (reason === 'invalid_scope' || reason === 'consent_required') {\n return new GmailTransportError('insufficient_scope', 'Microsoft refused a requested permission.', init)\n }\n return new GmailTransportError(\n status >= 500 ? 'unavailable' : 'invalid_request',\n `Microsoft’s token endpoint refused the request${aadsts ? ` (${aadsts})` : '.'}`,\n init,\n )\n}\n\nconst QUOTA = new Set(['ErrorQuotaExceeded', 'ErrorSubmissionQuotaExceeded', 'ErrorExceededMessageLimit'])\nconst SCOPE = new Set(['ErrorAccessDenied', 'AccessDenied', 'Authorization_RequestDenied'])\nconst NO_MAILBOX = new Set([\n 'MailboxNotEnabledForRESTAPI',\n 'MailboxNotHostedInExchangeOnline',\n 'ErrorMailboxNotEnabledForRESTAPI',\n])\nconst NOT_FOUND = new Set(['ErrorItemNotFound', 'ErrorInvalidIdMalformed', 'ResourceNotFound'])\n\n/** A Graph error response, mapped. */\nexport function graphApiError(\n status: number,\n body: unknown,\n init: { retryAfterMs?: number | null } = {},\n): GmailTransportError {\n const error = (body as { error?: { code?: unknown; message?: unknown } } | null)?.error\n const code = typeof error?.code === 'string' ? error.code : null\n const message = typeof error?.message === 'string' ? error.message : ''\n const detail = message ? `: ${message.slice(0, 200)}` : '.'\n const make = (transportCode: GmailTransportErrorCode, text: string) =>\n new GmailTransportError(transportCode, text, { status, providerReason: code, retryAfterMs: init.retryAfterMs })\n\n if (status === 429) return make('rate_limited', 'Microsoft is rate-limiting this mailbox. Try again later.')\n if (code && QUOTA.has(code)) return make('quota_exceeded', 'This mailbox has spent its Microsoft sending limit.')\n if (code && NO_MAILBOX.has(code)) {\n return make('mail_service_unavailable', 'This Microsoft account has no Exchange Online mailbox.')\n }\n if (status >= 500) return make('unavailable', `Microsoft Graph is unavailable${detail}`)\n if (status === 401) return make('unauthorized', 'Microsoft Graph refused the mailbox’s access token.')\n if (status === 403 && code && SCOPE.has(code)) {\n return make('insufficient_scope', 'The mailbox grant is missing a permission Sequences needs.')\n }\n if (status === 404 || (code && NOT_FOUND.has(code))) return make('not_found', `Microsoft Graph found nothing there${detail}`)\n if (status === 400) return make('invalid_request', `Microsoft Graph refused the request${detail}`)\n if (status === 403) return make('forbidden', `Microsoft Graph refused the request${detail}`)\n return make('unexpected', `Microsoft Graph answered ${status}${detail}`)\n}\n"],"names":["GmailTransportError","microsoftTokenEndpointError","status","body","record","reason","error","described","error_description","aadsts","exec","init","providerReason","QUOTA","Set","SCOPE","NO_MAILBOX","NOT_FOUND","graphApiError","code","message","detail","slice","make","transportCode","text","retryAfterMs","has"],"mappings":"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"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { type TransportDeps } from './http';
|
|
18
|
+
/**
|
|
19
|
+
* MICROSOFT'S IDENTITY PLATFORM, AS OUTREACH USES IT (AGL-3489).
|
|
20
|
+
*
|
|
21
|
+
* The authorization code flow with PKCE against a Microsoft Entra app
|
|
22
|
+
* registration, for a Microsoft 365 (Exchange Online) mailbox. Fetch-based
|
|
23
|
+
* and configuration-free like `google-oauth.ts`: the client id and secret,
|
|
24
|
+
* the tenant, the redirect address and the tokens are all handed in.
|
|
25
|
+
*
|
|
26
|
+
* Delegated permissions, all of them consented by the rep on the consent
|
|
27
|
+
* screen (or for the tenant by its administrator):
|
|
28
|
+
*
|
|
29
|
+
* - `Mail.Send` — sends as the rep.
|
|
30
|
+
* - `Mail.ReadWrite` — reads the replies and bounces a sequence stops on,
|
|
31
|
+
* and writes the draft a step is sent from, which is what gives a send
|
|
32
|
+
* the ids the next step and the sync need.
|
|
33
|
+
* - `User.Read` — the account's address and id, at connect.
|
|
34
|
+
* - `offline_access` — the refresh token the runtime sends with later.
|
|
35
|
+
* - `openid`, `email`, `profile` — the ID token naming the account.
|
|
36
|
+
*
|
|
37
|
+
* Microsoft has no endpoint that revokes one app's refresh token: a grant
|
|
38
|
+
* ends when the rep or the tenant administrator removes the app, or the
|
|
39
|
+
* token goes unused for its lifetime. Disconnecting deletes the stored
|
|
40
|
+
* grant, and says so.
|
|
41
|
+
*/
|
|
42
|
+
export declare const MICROSOFT_LOGIN_ORIGIN = "https://login.microsoftonline.com";
|
|
43
|
+
export declare const MICROSOFT_GRAPH_RESOURCE = "https://graph.microsoft.com";
|
|
44
|
+
export declare const GRAPH_MAIL_SEND_SCOPE = "https://graph.microsoft.com/Mail.Send";
|
|
45
|
+
export declare const GRAPH_MAIL_READWRITE_SCOPE = "https://graph.microsoft.com/Mail.ReadWrite";
|
|
46
|
+
export declare const GRAPH_USER_READ_SCOPE = "https://graph.microsoft.com/User.Read";
|
|
47
|
+
/** What a Microsoft mailbox grant asks for. See the module comment. */
|
|
48
|
+
export declare const OUTREACH_MICROSOFT_SCOPES: readonly ["openid", "email", "profile", "offline_access", "https://graph.microsoft.com/User.Read", "https://graph.microsoft.com/Mail.Send", "https://graph.microsoft.com/Mail.ReadWrite"];
|
|
49
|
+
/** The permissions a grant is useless without, by their short names. */
|
|
50
|
+
export declare const OUTREACH_MICROSOFT_REQUIRED_SCOPES: readonly ["mail.send", "mail.readwrite"];
|
|
51
|
+
/**
|
|
52
|
+
* The tenant segment of the endpoints: `common` (any work, school or
|
|
53
|
+
* personal account), `organizations` (work and school only), or one tenant's
|
|
54
|
+
* id or domain. Anything else is not a tenant this module will address.
|
|
55
|
+
*/
|
|
56
|
+
export declare function isMicrosoftTenant(value: string): boolean;
|
|
57
|
+
export declare function microsoftOAuthEndpoints(tenant: string): {
|
|
58
|
+
authorize: string;
|
|
59
|
+
token: string;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* The consent address a rep's browser is sent to. `prompt=select_account`
|
|
63
|
+
* lets a rep signed in to several Microsoft accounts choose the mailbox's;
|
|
64
|
+
* Microsoft issues a refresh token on every code exchange that asked for
|
|
65
|
+
* `offline_access`, so no consent prompt is forced.
|
|
66
|
+
*/
|
|
67
|
+
export declare function buildMicrosoftAuthorizationUrl(input: {
|
|
68
|
+
clientId: string;
|
|
69
|
+
tenant: string;
|
|
70
|
+
redirectUri: string;
|
|
71
|
+
state: string;
|
|
72
|
+
codeChallenge: string;
|
|
73
|
+
nonce: string;
|
|
74
|
+
loginHint?: string | null;
|
|
75
|
+
}): string;
|
|
76
|
+
export interface MicrosoftTokenResponse {
|
|
77
|
+
accessToken: string;
|
|
78
|
+
expiresInSeconds: number;
|
|
79
|
+
/**
|
|
80
|
+
* Issued on a code exchange, and again on most refreshes: Microsoft
|
|
81
|
+
* rotates refresh tokens, and the newest one is the one to keep.
|
|
82
|
+
*/
|
|
83
|
+
refreshToken: string | null;
|
|
84
|
+
/** Space-separated, as granted. */
|
|
85
|
+
scope: string;
|
|
86
|
+
idToken: string | null;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Exchanges an authorization code with its PKCE verifier. One attempt: a
|
|
90
|
+
* code is single-use, as at Google.
|
|
91
|
+
*/
|
|
92
|
+
export declare function exchangeMicrosoftAuthorizationCode(input: {
|
|
93
|
+
clientId: string;
|
|
94
|
+
clientSecret: string;
|
|
95
|
+
tenant: string;
|
|
96
|
+
code: string;
|
|
97
|
+
redirectUri: string;
|
|
98
|
+
codeVerifier: string;
|
|
99
|
+
}, deps?: TransportDeps): Promise<MicrosoftTokenResponse>;
|
|
100
|
+
/** Mints an access token for Graph from a refresh token. */
|
|
101
|
+
export declare function refreshMicrosoftAccessToken(input: {
|
|
102
|
+
clientId: string;
|
|
103
|
+
clientSecret: string;
|
|
104
|
+
tenant: string;
|
|
105
|
+
refreshToken: string;
|
|
106
|
+
}, deps?: TransportDeps): Promise<MicrosoftTokenResponse>;
|
|
107
|
+
/**
|
|
108
|
+
* Whether a granted scope string carries every permission Sequences needs.
|
|
109
|
+
* Microsoft lists Graph permissions by their short names (`Mail.Send`) or in
|
|
110
|
+
* full (`https://graph.microsoft.com/Mail.Send`), and either counts.
|
|
111
|
+
*/
|
|
112
|
+
export declare function microsoftScopesInclude(scope: string, required?: readonly string[]): boolean;
|
|
113
|
+
/** The ID token claims Outreach relies on. */
|
|
114
|
+
export interface MicrosoftIdentity {
|
|
115
|
+
/** `<tid>:<oid>` — the account's stable id, unique across tenants. */
|
|
116
|
+
sub: string;
|
|
117
|
+
/** The directory object id: what Graph's `/me` answers as `id`. */
|
|
118
|
+
oid: string;
|
|
119
|
+
tid: string;
|
|
120
|
+
/** The sign-in name, lowercased; the mailbox address comes from Graph. */
|
|
121
|
+
username: string;
|
|
122
|
+
}
|
|
123
|
+
export type MicrosoftIdTokenRefusal = 'malformed' | 'issuer' | 'audience' | 'expired' | 'nonce';
|
|
124
|
+
/**
|
|
125
|
+
* The identity an ID token asserts, after the checks OpenID Connect requires
|
|
126
|
+
* of a token read off the token endpoint's own TLS response (OIDC Core
|
|
127
|
+
* §3.1.3.7, as for Google): issued by Microsoft for the tenant it names, to
|
|
128
|
+
* this app, unexpired, and carrying this connect's nonce.
|
|
129
|
+
*/
|
|
130
|
+
export declare function readMicrosoftIdToken(idToken: string | null | undefined, expected: {
|
|
131
|
+
clientId: string;
|
|
132
|
+
nonce: string;
|
|
133
|
+
nowMs: number;
|
|
134
|
+
}): {
|
|
135
|
+
ok: true;
|
|
136
|
+
identity: MicrosoftIdentity;
|
|
137
|
+
} | {
|
|
138
|
+
ok: false;
|
|
139
|
+
refusal: MicrosoftIdTokenRefusal;
|
|
140
|
+
};
|