@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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/mailboxes/mailbox-routes.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 { normalizeContactEmail } from '@aglyn/aglyn/app-utils/contacts'\nimport type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport {\n googleMailboxSenderExpectation,\n type SenderReadiness,\n type SenderReadinessExpectation,\n} from '@aglyn/shared-util-email'\nimport type {\n OutreachMailbox,\n OutreachMailboxStatus,\n} from '../model/outreach.types'\nimport { createGmailClient } from '../transport/gmail-client'\nimport { GmailTransportError, googleFailureAnswer } from '../transport/gmail-errors'\nimport {\n buildGoogleAuthorizationUrl,\n exchangeGoogleAuthorizationCode,\n grantedScopesInclude,\n OUTREACH_REQUIRED_GRANTED_SCOPES,\n pkceChallenge,\n readGoogleIdToken,\n type GoogleTokenResponse,\n} from '../transport/google-oauth'\nimport type { TransportDeps } from '../transport/http'\nimport { Rfc5322MessageError } from '../transport/rfc5322'\nimport { sendOutreachMessage } from '../transport/send-message'\nimport {\n buildConnectReturnFragment,\n type OutreachApiRefusalReason,\n type OutreachConnectReturn,\n type OutreachMailboxAvailability,\n type OutreachMailboxAvailabilityGate,\n type OutreachMailboxDisconnectResponse,\n type OutreachMailboxReadinessResponse,\n} from './mailbox-api'\nimport {\n mailboxCredentialsRef,\n mailboxRef,\n outreachMailboxId,\n readMailboxCredentials,\n sealMailboxRefreshToken,\n type OutreachGoogleMailboxCredentials,\n} from './mailbox-credentials'\nimport { revokeMailboxGrant } from './mailbox-revoke'\nimport {\n outreachMemberGate,\n refusal,\n type OutreachGateContext,\n type OutreachGateDeps,\n} from './mailbox-gate'\nimport {\n defaultSendAs,\n isValidTimezone,\n OUTREACH_DEFAULT_DAILY_CAP,\n OUTREACH_DEFAULT_WINDOW,\n OUTREACH_MAX_MAILBOXES_PER_MEMBER,\n validateDailyCap,\n validateDisplayName,\n validateSendWindow,\n verifiedSendAsAddresses,\n} from './mailbox-settings'\nimport {\n markOutreachMailboxReconnectRequired,\n openOutreachMailboxClient,\n} from './mailbox-transport'\nimport {\n consumeOutreachOAuthState,\n mintOutreachOAuthState,\n outreachOidcNonce,\n outreachPkceVerifier,\n readOutreachOAuthState,\n recordOutreachOAuthState,\n} from './oauth-state'\nimport {\n OUTREACH_NOT_CONFIGURED_MESSAGE,\n type OutreachGoogleConfigResult,\n} from './outreach-config'\n\n/**\n * THE MAILBOX ROUTES (AGL-2978): connect a rep's Google mailbox, keep its\n * settings, pause it, test it, and disconnect it.\n *\n * ## The connect, end to end\n *\n * 1. `connect` (the member's session) mints a signed, single-use state bound\n * to the member and the organization, records its pending entry, and\n * answers Google's consent address — offline access, the consent prompt,\n * a PKCE challenge and an OpenID nonce, both derived from the state.\n * 2. Google redirects the browser to `oauth/callback`. That request carries\n * no bearer token, so it DOES nothing: it verifies the state's signature,\n * finds the organization's console path, and redirects to the Mailboxes\n * page with the code in the URL FRAGMENT, where no server and no `Referer`\n * sees it. Its release gate was asked about the organization and the\n * member the state names (the route's declared subject).\n * 3. The page takes the code out of the address bar and posts it to\n * `connect/complete` with the member's own session. That is where the\n * state is consumed, where it is refused if it was minted for anyone else,\n * and where the code is exchanged. So a consent screen someone else was\n * tricked into completing cannot connect THEIR mailbox to the member who\n * started it: the code arrives in the wrong browser, whose session is not\n * the state's member.\n *\n * ## Refusals, in order, on `connect/complete`\n *\n * A state that is invalid, minted for another member or organization,\n * expired, already used or superseded; a code Google refuses; a grant with no\n * refresh token; a grant missing `gmail.send` or `gmail.readonly` (the consent\n * screen lets a person untick them); an ID token that is not about this\n * exchange or whose address Google has not verified; an account with no Gmail\n * service (a Workspace user whose Gmail is off, or not yet provisioned) or one\n * Google bars from the Gmail API; a Gmail profile whose address is not the ID\n * token's; and a member at their mailbox limit. Each is a 4xx: only a Google\n * outage, or a request Google calls malformed, answers 502.\n *\n * A grant refused after the code exchange is dropped, not revoked. Google's\n * revocation ends the whole grant this client holds for the account, and the\n * same account may already be connected — by this member in another\n * organization, or by a teammate — so revoking a token this route merely\n * declines to keep could disconnect a mailbox that is working.\n *\n * ## Who may do what\n *\n * Every route but the callback climbs `outreachMemberGate`. The member who\n * connected a mailbox manages it; an organization owner or admin may also\n * change its settings, pause it and disconnect it — a departing rep's mailbox\n * has to be stoppable by somebody. Only its own member can send a test from\n * it, since the test lands in their inbox.\n */\n\nexport interface OutreachMailboxRouteDeps {\n firestore(): FirebaseFirestore.Firestore\n gate: OutreachGateDeps\n readConfig(): OutreachGoogleConfigResult\n /** Whether the signing secret the OAuth state needs is configured. */\n stateSigningConfigured(): boolean\n redirectUri(requestUrl: string): string | null\n now(): number\n /** The network and retry policy every Google call uses. */\n transport: TransportDeps\n consumeRateLimit(key: string, options: { limit: number; windowMs: number }): Promise<{ allowed: boolean }>\n logOrgActivity(\n orgId: string,\n actor: { uid: string; email?: string | null },\n action: string,\n target: { type: typeof OUTREACH_MAILBOX_ACTIVITY_TARGET; id: string; name: string },\n ): Promise<void>\n confirmAliasesByProvider(\n firestore: FirebaseFirestore.Firestore,\n input: { orgId: string; uid: string; addresses: readonly string[]; nowMs: number },\n ): Promise<{ ok: boolean; confirmed?: string[] }>\n /** SPF, DKIM and DMARC for one sender, looked up and briefly remembered. */\n readSenderReadiness(expectation: SenderReadinessExpectation, options: { fresh?: boolean }): Promise<SenderReadiness>\n}\n\nexport interface OutreachMailboxRoutes {\n availability: PluginWebApiHandler\n connect: PluginWebApiHandler\n oauthCallback: PluginWebApiHandler\n connectComplete: PluginWebApiHandler\n settings: PluginWebApiHandler\n status: PluginWebApiHandler\n test: PluginWebApiHandler\n disconnect: PluginWebApiHandler\n readiness: PluginWebApiHandler\n}\n\n/** Test sends one mailbox may make in an hour. */\nexport const OUTREACH_TEST_SENDS_PER_HOUR = 5\n\n/**\n * The org activity target a mailbox's rows are filed under: Outreach's own\n * namespaced resource, which the feed reads as \"Mailbox\".\n */\nexport const OUTREACH_MAILBOX_ACTIVITY_TARGET = 'outreach:mailbox'\n\nconst MAILBOX_ID = /^[A-Za-z0-9_-]{1,128}$/\nconst MAX_CODE_CHARS = 2048\n\nconst noStore = { 'Cache-Control': 'no-store' }\n\nconst ok = (body: unknown) => Response.json(body, { status: 200, headers: noStore })\n\nfunction methodNotAllowed(allow: string): Response {\n return Response.json(\n { error: 'Method not allowed', reason: 'method-not-allowed' satisfies OutreachApiRefusalReason },\n { status: 405, headers: { ...noStore, Allow: allow } },\n )\n}\n\nasync function readBody(request: Request): Promise<Record<string, unknown>> {\n const body = await request.json().catch(() => null)\n return body && typeof body === 'object' && !Array.isArray(body) ? (body as Record<string, unknown>) : {}\n}\n\nfunction notConfigured(): Response {\n return refusal(503, 'not-configured', OUTREACH_NOT_CONFIGURED_MESSAGE)\n}\n\n/** A mailbox document read defensively into the model shape. */\nfunction readMailbox(id: string, data: Record<string, unknown> | undefined): OutreachMailbox | null {\n if (!data) return null\n return { ...(data as unknown as OutreachMailbox), id }\n}\n\nconst activityName = (mailbox: Pick<OutreachMailbox, 'email'>) => mailbox.email\n\nexport function createOutreachMailboxRoutes(deps: OutreachMailboxRouteDeps): OutreachMailboxRoutes {\n /** The gate, then the mailbox the body names, then who may touch it. */\n async function loadManagedMailbox(\n request: Request,\n body: Record<string, unknown>,\n who: 'member-or-admin' | 'member',\n ): Promise<Response | { gate: OutreachGateContext; mailbox: OutreachMailbox }> {\n const gate = await outreachMemberGate(request, body['orgId'], deps.gate)\n if (gate instanceof Response) return gate\n const mailboxId = typeof body['mailboxId'] === 'string' ? body['mailboxId'] : ''\n if (!MAILBOX_ID.test(mailboxId)) return refusal(400, 'invalid-request', 'Name the mailbox.')\n const snapshot = await mailboxRef(deps.firestore(), gate.orgId, mailboxId).get()\n const mailbox = readMailbox(mailboxId, snapshot.exists ? snapshot.data() : undefined)\n if (!mailbox) return refusal(404, 'mailbox-not-found', 'That mailbox is not connected to this organization.')\n const own = mailbox.connectedByUid === gate.uid\n if (!own && !(who === 'member-or-admin' && gate.isOrgAdmin)) {\n return refusal(\n 403,\n 'not-your-mailbox',\n who === 'member'\n ? 'Only the member who connected this mailbox can do that.'\n : 'Only the member who connected this mailbox, or an organization owner or admin, can change it.',\n )\n }\n return { gate, mailbox }\n }\n\n const availability: PluginWebApiHandler = async (request) => {\n if (request.method !== 'GET') return methodNotAllowed('GET')\n const gate = await outreachMemberGate(request, new URL(request.url).searchParams.get('orgId'), deps.gate)\n if (gate instanceof Response) return gate\n // Every gate is named, not the first: an operator fixing a deployment\n // should see the whole list once rather than one variable per redeploy.\n const missing: OutreachMailboxAvailabilityGate[] = []\n const config = deps.readConfig()\n if (config.configured === false) missing.push({ gate: 'google', missing: config.missing })\n if (!deps.stateSigningConfigured()) missing.push({ gate: 'state' })\n if (!deps.redirectUri(request.url)) missing.push({ gate: 'redirect' })\n return ok({\n configured: missing.length === 0,\n canManageAll: gate.isOrgAdmin,\n ...(missing.length ? { missing } : {}),\n } satisfies OutreachMailboxAvailability)\n }\n\n const connect: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return methodNotAllowed('POST')\n const body = await readBody(request)\n const gate = await outreachMemberGate(request, body['orgId'], deps.gate)\n if (gate instanceof Response) return gate\n const config = deps.readConfig()\n const redirectUri = deps.redirectUri(request.url)\n if (!config.configured || !redirectUri || !deps.stateSigningConfigured()) return notConfigured()\n\n const nowMs = deps.now()\n const { state, claims } = mintOutreachOAuthState({ orgId: gate.orgId, uid: gate.uid, nowMs })\n await recordOutreachOAuthState(deps.firestore(), { claims, redirectUri, nowMs })\n const url = buildGoogleAuthorizationUrl({\n clientId: config.config.clientId,\n redirectUri,\n state,\n codeChallenge: pkceChallenge(outreachPkceVerifier(claims.nonce)),\n nonce: outreachOidcNonce(claims.nonce),\n loginHint: gate.email,\n })\n return ok({ url })\n }\n\n const oauthCallback: PluginWebApiHandler = async (request) => {\n if (request.method !== 'GET') return methodNotAllowed('GET')\n const params = new URL(request.url).searchParams\n const read = readOutreachOAuthState(params.get('state'), deps.now())\n const plain = (status: number, text: string) =>\n new Response(text, {\n status,\n headers: { ...noStore, 'Content-Type': 'text/plain; charset=utf-8', 'Referrer-Policy': 'no-referrer' },\n })\n if (read.ok === false && read.refusal === 'state-invalid') {\n return plain(400, 'This connection link is not valid. Return to the console and connect the mailbox again.')\n }\n const claims = read.ok ? read.claims : (read as { claims: { orgId: string } }).claims\n const org = await deps.gate.readOrg(claims.orgId)\n const slug = typeof org?.['slug'] === 'string' ? org['slug'] : ''\n if (!slug) return plain(404, 'That organization could not be found.')\n\n let fragment: OutreachConnectReturn\n const googleError = params.get('error')\n const code = params.get('code') ?? ''\n if (googleError) {\n fragment = { kind: 'error', reason: googleError === 'access_denied' ? 'access_denied' : 'google_error' }\n } else if (!read.ok) {\n fragment = { kind: 'error', reason: 'expired' }\n } else if (!code || code.length > MAX_CODE_CHARS) {\n fragment = { kind: 'error', reason: 'google_error' }\n } else {\n fragment = { kind: 'code', code, state: params.get('state') ?? '' }\n }\n // A RELATIVE location on the callback's own origin: the path is built from\n // the organization's slug, never from anything the request supplied.\n return new Response(null, {\n status: 303,\n headers: {\n ...noStore,\n 'Referrer-Policy': 'no-referrer',\n Location: `/${encodeURIComponent(slug)}/outreach/mailboxes#${buildConnectReturnFragment(fragment)}`,\n },\n })\n }\n\n const connectComplete: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return methodNotAllowed('POST')\n const body = await readBody(request)\n const gate = await outreachMemberGate(request, body['orgId'], deps.gate)\n if (gate instanceof Response) return gate\n const config = deps.readConfig()\n if (!config.configured || !deps.stateSigningConfigured()) return notConfigured()\n const code = typeof body['code'] === 'string' ? body['code'] : ''\n if (!code || code.length > MAX_CODE_CHARS) return refusal(400, 'invalid-request', 'The connection did not return a code.')\n\n const firestore = deps.firestore()\n const nowMs = deps.now()\n const read = readOutreachOAuthState(body['state'], nowMs)\n if (read.ok === false && read.refusal === 'state-invalid') {\n return refusal(400, 'state-invalid', 'This connection could not be verified. Connect the mailbox again.')\n }\n const claims = read.ok ? read.claims : (read as { claims: typeof read.claims }).claims\n if (claims.uid !== gate.uid) {\n return refusal(\n 403,\n 'state-user-mismatch',\n 'This connection was started by another member. Sign in as that member, or connect the mailbox again.',\n )\n }\n if (claims.orgId !== gate.orgId) {\n return refusal(400, 'state-org-mismatch', 'This connection was started in another organization.')\n }\n if (!read.ok) return refusal(410, 'state-expired', 'This connection took too long. Connect the mailbox again.')\n\n const consumed = await consumeOutreachOAuthState(firestore, { claims, nowMs })\n if (consumed.ok === false) {\n if (consumed.refusal === 'state-expired') {\n return refusal(410, 'state-expired', 'This connection took too long. Connect the mailbox again.')\n }\n return refusal(\n 409,\n consumed.refusal,\n consumed.refusal === 'state-superseded'\n ? 'A newer connection was started. Finish that one, or connect the mailbox again.'\n : 'This connection was already used. Connect the mailbox again.',\n )\n }\n\n let grant: GoogleTokenResponse\n try {\n grant = await exchangeGoogleAuthorizationCode(\n {\n clientId: config.config.clientId,\n clientSecret: config.config.clientSecret,\n code,\n redirectUri: consumed.redirectUri,\n codeVerifier: outreachPkceVerifier(claims.nonce),\n },\n deps.transport,\n )\n } catch (error) {\n if (error instanceof GmailTransportError && error.code === 'invalid_grant') {\n return refusal(400, 'code-rejected', \"Google's sign-in expired or was already used. Connect the mailbox again.\")\n }\n if (error instanceof GmailTransportError && error.code === 'client_misconfigured') {\n return refusal(503, 'not-configured', \"Google refused this deployment's OAuth client.\")\n }\n if (error instanceof GmailTransportError && error.code === 'insufficient_scope') {\n return refusal(\n 422,\n 'scopes-missing',\n 'Sequences needs permission to send and to read your mail. Connect again and allow both.',\n )\n }\n return googleFailure(error, 'connect/complete token exchange')\n }\n if (!grant.refreshToken) {\n return refusal(\n 422,\n 'refresh-token-missing',\n 'Google did not grant offline access, so the mailbox could not send later. Connect it again.',\n )\n }\n if (!grantedScopesInclude(grant.scope, OUTREACH_REQUIRED_GRANTED_SCOPES)) {\n return refusal(\n 422,\n 'scopes-missing',\n 'Sequences needs permission to send and to read your mail. Connect again and allow both.',\n )\n }\n const identity = readGoogleIdToken(grant.idToken, {\n clientId: config.config.clientId,\n nonce: outreachOidcNonce(claims.nonce),\n nowMs,\n })\n if (identity.ok === false) {\n return refusal(\n 422,\n 'identity-unverified',\n identity.refusal === 'email-unverified'\n ? \"Google has not verified this account's email address.\"\n : \"Google's answer could not be verified. Connect the mailbox again.\",\n )\n }\n\n const client = createGmailClient({\n ...deps.transport,\n clientId: config.config.clientId,\n clientSecret: config.config.clientSecret,\n refreshToken: grant.refreshToken,\n })\n let profileEmail: string\n let sendAsOptions: ReturnType<typeof verifiedSendAsAddresses>\n try {\n const [profile, sendAs] = await Promise.all([client.getProfile(), client.listSendAs()])\n profileEmail = profile.emailAddress\n sendAsOptions = verifiedSendAsAddresses(sendAs)\n } catch (error) {\n if (error instanceof GmailTransportError && error.reconnectRequired) {\n return refusal(\n 422,\n 'scopes-missing',\n 'Sequences needs permission to send and to read your mail. Connect again and allow both.',\n )\n }\n return googleFailure(error, 'connect/complete Gmail profile')\n }\n if (profileEmail !== identity.identity.email) {\n return refusal(422, 'account-mismatch', 'The Gmail account and the Google sign-in did not match. Connect again.')\n }\n if (!sendAsOptions.some((option) => option.email === profileEmail)) {\n sendAsOptions = [\n { email: profileEmail, displayName: '', isPrimary: true, isDefault: !sendAsOptions.some((o) => o.isDefault) },\n ...sendAsOptions,\n ]\n }\n\n const mailboxId = outreachMailboxId(gate.orgId, gate.uid, identity.identity.sub)\n const mailboxDoc = mailboxRef(firestore, gate.orgId, mailboxId)\n const credentialDoc = mailboxCredentialsRef(firestore, mailboxId)\n const [existingMailbox, existingCredential] = await Promise.all([mailboxDoc.get(), credentialDoc.get()])\n const current = readMailbox(mailboxId, existingMailbox.exists ? existingMailbox.data() : undefined)\n if (!current) {\n const mine = await firestore\n .collection('orgs')\n .doc(gate.orgId)\n .collection('outreachMailboxes')\n .where('connectedByUid', '==', gate.uid)\n .limit(OUTREACH_MAX_MAILBOXES_PER_MEMBER)\n .get()\n if (mine.size >= OUTREACH_MAX_MAILBOXES_PER_MEMBER) {\n return refusal(\n 409,\n 'mailbox-limit',\n `You can connect up to ${OUTREACH_MAX_MAILBOXES_PER_MEMBER} mailboxes. Disconnect one before connecting another.`,\n )\n }\n }\n\n const defaultOption = sendAsOptions.find((option) => option.email === defaultSendAs(sendAsOptions, profileEmail))\n let mailbox: OutreachMailbox\n /** On a reconnect, only the fields a connect decides — see the write below. */\n let reconnected: Partial<OutreachMailbox> | null = null\n if (current) {\n const keepSendAs = sendAsOptions.some((option) => option.email === current.sendAs)\n reconnected = {\n provider: 'google',\n email: profileEmail,\n sendAs: keepSendAs ? current.sendAs : (defaultOption?.email ?? profileEmail),\n sendAsOptions,\n // A reconnect restores a mailbox Google had cut off, and leaves a\n // mailbox its member paused paused.\n status: (current.status === 'paused' ? 'paused' : 'connected') satisfies OutreachMailboxStatus,\n connectedAtMs: nowMs,\n updatedAtMs: nowMs,\n }\n mailbox = { ...current, ...reconnected }\n } else {\n const timezone = isValidTimezone(body['timezone']) ? String(body['timezone']) : 'UTC'\n const displayName = validateDisplayName(defaultOption?.displayName || gate.name || '')\n mailbox = {\n id: mailboxId,\n provider: 'google',\n email: profileEmail,\n sendAs: defaultOption?.email ?? profileEmail,\n sendAsOptions,\n displayName: typeof displayName === 'string' ? displayName : '',\n status: 'connected',\n dailyCap: OUTREACH_DEFAULT_DAILY_CAP,\n window: { ...OUTREACH_DEFAULT_WINDOW, days: [...OUTREACH_DEFAULT_WINDOW.days] },\n timezone,\n rampStartedAtMs: nowMs,\n health: {\n sentToday: 0,\n sentOnDay: null,\n bounces: 0,\n replies: 0,\n lastSentAtMs: null,\n lastErrorAtMs: null,\n lastErrorCode: null,\n daily: {},\n },\n connectedByUid: gate.uid,\n connectedAtMs: nowMs,\n createdAtMs: nowMs,\n updatedAtMs: nowMs,\n }\n }\n const credential: OutreachGoogleMailboxCredentials = {\n id: mailboxId,\n orgId: gate.orgId,\n mailboxId,\n provider: 'google',\n connectedByUid: gate.uid,\n providerAccountId: identity.identity.sub,\n email: profileEmail,\n scopes: grant.scope.split(/\\s+/).filter(Boolean),\n ...sealMailboxRefreshToken(grant.refreshToken, mailboxId, config.config.keyring),\n createdAtMs: Number(existingCredential.exists ? existingCredential.get('createdAtMs') : 0) || nowMs,\n updatedAtMs: nowMs,\n }\n const batch = firestore.batch()\n // A reconnect MERGES only what a connect decides, so the settings a member\n // chose and the health counters the sending runtime increments are never\n // written back from a read that may already be stale.\n if (reconnected) batch.set(mailboxDoc, reconnected, { merge: true })\n else batch.set(mailboxDoc, mailbox)\n batch.set(credentialDoc, credential)\n await batch.commit()\n\n const aliases: { ok: boolean; confirmed?: string[] } = await deps\n .confirmAliasesByProvider(firestore, {\n orgId: gate.orgId,\n uid: gate.uid,\n addresses: sendAsOptions.map((option) => option.email),\n nowMs,\n })\n .catch((error: unknown) => {\n console.error('[outreach] confirming member aliases from send-as failed', error)\n return { ok: false }\n })\n await deps.logOrgActivity(\n gate.orgId,\n { uid: gate.uid, email: gate.email },\n current ? 'Reconnected a mailbox in Sequences' : 'Connected a mailbox in Sequences',\n { type: OUTREACH_MAILBOX_ACTIVITY_TARGET, id: mailboxId, name: activityName(mailbox) },\n )\n return ok({\n ok: true,\n mailbox,\n created: !current,\n confirmedAliases: aliases.ok ? (aliases.confirmed ?? []) : [],\n })\n }\n\n const settings: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return methodNotAllowed('POST')\n const body = await readBody(request)\n const loaded = await loadManagedMailbox(request, body, 'member-or-admin')\n if (loaded instanceof Response) return loaded\n const { gate, mailbox } = loaded\n const update: Partial<OutreachMailbox> = {}\n const invalid = (message: string) => refusal(400, 'invalid-settings', message)\n\n if (body['sendAs'] !== undefined) {\n const sendAs = typeof body['sendAs'] === 'string' ? body['sendAs'].trim().toLowerCase() : ''\n if (!(mailbox.sendAsOptions ?? []).some((option) => option.email === sendAs)) {\n return invalid('Choose one of the addresses Gmail has verified for this account.')\n }\n update.sendAs = sendAs\n }\n if (body['displayName'] !== undefined) {\n const name = validateDisplayName(body['displayName'])\n if (typeof name !== 'string') return invalid(name.message)\n update.displayName = name\n }\n if (body['dailyCap'] !== undefined) {\n const cap = validateDailyCap(body['dailyCap'])\n if (typeof cap !== 'number') return invalid(cap.message)\n update.dailyCap = cap\n }\n if (body['window'] !== undefined) {\n const window = validateSendWindow(body['window'])\n if (!('days' in window)) return invalid(window.message)\n update.window = window\n }\n if (body['timezone'] !== undefined) {\n if (!isValidTimezone(body['timezone'])) return invalid('Choose a timezone from the list.')\n update.timezone = body['timezone']\n }\n if (body['warmUp'] !== undefined) {\n /*\n * The ramp guards a NEW sender's reputation; a mailbox that has sent\n * mail for years has one already, and ramping it to ten a day guards\n * nothing (AGL-3228). Its member decides. Turning the ramp back on\n * starts a fresh one from today, which is what \"warm up\" means.\n */\n if (typeof body['warmUp'] !== 'boolean') return invalid('Say whether to warm the mailbox up gradually.')\n const running = typeof mailbox.rampStartedAtMs === 'number'\n if (body['warmUp'] && !running) update.rampStartedAtMs = deps.now()\n if (!body['warmUp'] && running) update.rampStartedAtMs = null\n }\n if (!Object.keys(update).length) return ok({ ok: true, mailbox })\n const nowMs = deps.now()\n await mailboxRef(deps.firestore(), gate.orgId, mailbox.id).update({ ...update, updatedAtMs: nowMs })\n return ok({ ok: true, mailbox: { ...mailbox, ...update, updatedAtMs: nowMs } })\n }\n\n const status: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return methodNotAllowed('POST')\n const body = await readBody(request)\n if (typeof body['paused'] !== 'boolean') return refusal(400, 'invalid-request', 'Say whether to pause the mailbox.')\n const loaded = await loadManagedMailbox(request, body, 'member-or-admin')\n if (loaded instanceof Response) return loaded\n const { gate, mailbox } = loaded\n if (mailbox.status === 'reconnect_required' || mailbox.status === 'disconnected') {\n return refusal(409, 'reconnect-required', 'Reconnect this mailbox before pausing or resuming it.')\n }\n const next: OutreachMailboxStatus = body['paused'] ? 'paused' : 'connected'\n if (mailbox.status === next) return ok({ ok: true, mailbox })\n const nowMs = deps.now()\n // A member's pause or resume answers an automatic pause (AGL-2981): its\n // reason goes, and the next bounce is judged as new evidence.\n await mailboxRef(deps.firestore(), gate.orgId, mailbox.id).update({\n status: next,\n autoPause: null,\n updatedAtMs: nowMs,\n })\n await deps.logOrgActivity(\n gate.orgId,\n { uid: gate.uid, email: gate.email },\n next === 'paused' ? 'Paused a mailbox in Sequences' : 'Resumed a mailbox in Sequences',\n { type: OUTREACH_MAILBOX_ACTIVITY_TARGET, id: mailbox.id, name: activityName(mailbox) },\n )\n return ok({ ok: true, mailbox: { ...mailbox, status: next, autoPause: null, updatedAtMs: nowMs } })\n }\n\n const test: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return methodNotAllowed('POST')\n const body = await readBody(request)\n const loaded = await loadManagedMailbox(request, body, 'member')\n if (loaded instanceof Response) return loaded\n const { gate, mailbox } = loaded\n if (mailbox.status === 'reconnect_required' || mailbox.status === 'disconnected') {\n return refusal(409, 'reconnect-required', 'Reconnect this mailbox before sending from it.')\n }\n if (!deps.readConfig().configured) return notConfigured()\n const limited = await deps.consumeRateLimit(`outreach-mailbox-test:${mailbox.id}`, {\n limit: OUTREACH_TEST_SENDS_PER_HOUR,\n windowMs: 60 * 60 * 1000,\n })\n if (!limited.allowed) {\n return refusal(429, 'rate-limited', `A mailbox can send ${OUTREACH_TEST_SENDS_PER_HOUR} tests an hour. Try again later.`)\n }\n /*\n * To the member's own address unless they name another (AGL-3228). A\n * test to oneself never leaves Google, so it arrives with no\n * Authentication-Results at all and cannot show whether the send-as\n * domain's DKIM and DMARC hold; an outside mailbox the member can read\n * is the only place that can be seen. The rate limit above is the\n * whole guard on who may be written to — it is the member's own\n * account sending, as it could from its own compose window.\n */\n const named = body['to']\n const to =\n named === undefined || named === null || String(named).trim() === ''\n ? mailbox.email\n : normalizeContactEmail(String(named))\n if (!to) {\n return refusal(400, 'invalid-request', `\"${String(named).trim()}\" isn't a valid email address.`)\n }\n const firestore = deps.firestore()\n const nowMs = deps.now()\n const opened = await openOutreachMailboxClient(\n firestore,\n { mailboxId: mailbox.id },\n { ...deps.transport, readConfig: deps.readConfig, now: deps.now },\n )\n if (opened.ok === false) {\n if (opened.reason === 'not-configured') return notConfigured()\n await markOutreachMailboxReconnectRequired(firestore, {\n orgId: gate.orgId,\n mailboxId: mailbox.id,\n errorCode: opened.reason,\n nowMs,\n })\n return refusal(409, 'reconnect-required', 'This mailbox needs to be connected again before it can send.')\n }\n try {\n const sent = await sendOutreachMessage(opened.client, {\n from: { address: mailbox.sendAs, name: mailbox.displayName },\n to,\n subject: 'Sequences test message',\n text: [\n 'This is a test message from Sequences.',\n '',\n `It was sent through the connected mailbox ${mailbox.email} as ${mailbox.sendAs}. ` +\n 'If it reached the inbox, this mailbox can send.',\n ...(to === mailbox.email\n ? []\n : [\n '',\n 'To read whether the sender is authenticated, open this message\\u2019s original source ' +\n 'and look for dkim=pass, spf=pass and dmarc=pass in Authentication-Results.',\n ]),\n ].join('\\n'),\n })\n return ok({ ok: true, sentTo: to, gmailMessageId: sent.gmailMessageId, sentAtMs: nowMs })\n } catch (error) {\n if (error instanceof GmailTransportError && error.reconnectRequired) {\n await markOutreachMailboxReconnectRequired(firestore, {\n orgId: gate.orgId,\n mailboxId: mailbox.id,\n errorCode: error.code,\n nowMs,\n })\n return refusal(409, 'reconnect-required', 'Google refused this mailbox’s access. Connect it again.')\n }\n if (error instanceof Rfc5322MessageError) {\n return refusal(400, 'invalid-settings', `The test could not be written: ${error.message}`)\n }\n return googleFailure(error, 'test send')\n }\n }\n\n const disconnect: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return methodNotAllowed('POST')\n const body = await readBody(request)\n const loaded = await loadManagedMailbox(request, body, 'member-or-admin')\n if (loaded instanceof Response) return loaded\n const { gate, mailbox } = loaded\n const firestore = deps.firestore()\n const credentialDoc = mailboxCredentialsRef(firestore, mailbox.id)\n const snapshot = await credentialDoc.get()\n const credential = readMailboxCredentials(snapshot.exists ? snapshot.data() : null)\n const revocation = credential\n ? await revokeMailboxGrant(firestore, credential, deps)\n : ('already-invalid' as const)\n const batch = firestore.batch()\n batch.delete(credentialDoc)\n batch.delete(mailboxRef(firestore, gate.orgId, mailbox.id))\n await batch.commit()\n await deps.logOrgActivity(\n gate.orgId,\n { uid: gate.uid, email: gate.email },\n 'Disconnected a mailbox in Sequences',\n { type: OUTREACH_MAILBOX_ACTIVITY_TARGET, id: mailbox.id, name: activityName(mailbox) },\n )\n return ok({ ok: true, revocation } satisfies OutreachMailboxDisconnectResponse)\n }\n\n /**\n * How receivers judge mail from the address a mailbox sends as. Any member\n * who can see the mailbox can read it: it is a reading of public DNS about\n * the organization's own domain, and changes nothing.\n */\n const readiness: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return methodNotAllowed('POST')\n const body = await readBody(request)\n const gate = await outreachMemberGate(request, body['orgId'], deps.gate)\n if (gate instanceof Response) return gate\n const mailboxId = typeof body['mailboxId'] === 'string' ? body['mailboxId'] : ''\n if (!MAILBOX_ID.test(mailboxId)) return refusal(400, 'invalid-request', 'Name the mailbox.')\n const snapshot = await mailboxRef(deps.firestore(), gate.orgId, mailboxId).get()\n const mailbox = readMailbox(mailboxId, snapshot.exists ? snapshot.data() : undefined)\n if (!mailbox) return refusal(404, 'mailbox-not-found', 'That mailbox is not connected to this organization.')\n const sendAs = mailbox.sendAs || mailbox.email\n const expectation = googleMailboxSenderExpectation(sendAs)\n const read = expectation\n ? await deps.readSenderReadiness(expectation, { fresh: body['fresh'] === true }).catch(() => null)\n : null\n return ok({ ok: true, sendAs, readiness: read } satisfies OutreachMailboxReadinessResponse)\n }\n\n return { availability, connect, oauthCallback, connectComplete, settings, status, test, disconnect, readiness }\n}\n\n/** A Google failure the route could not handle itself, as the refusal {@link googleFailureAnswer} decides. */\nfunction googleFailure(error: unknown, route: string): Response {\n const answer = googleFailureAnswer(error, route)\n return refusal(answer.status, answer.reason, answer.error)\n}\n"],"names":["normalizeContactEmail","googleMailboxSenderExpectation","createGmailClient","GmailTransportError","googleFailureAnswer","buildGoogleAuthorizationUrl","exchangeGoogleAuthorizationCode","grantedScopesInclude","OUTREACH_REQUIRED_GRANTED_SCOPES","pkceChallenge","readGoogleIdToken","Rfc5322MessageError","sendOutreachMessage","buildConnectReturnFragment","mailboxCredentialsRef","mailboxRef","outreachMailboxId","readMailboxCredentials","sealMailboxRefreshToken","revokeMailboxGrant","outreachMemberGate","refusal","defaultSendAs","isValidTimezone","OUTREACH_DEFAULT_DAILY_CAP","OUTREACH_DEFAULT_WINDOW","OUTREACH_MAX_MAILBOXES_PER_MEMBER","validateDailyCap","validateDisplayName","validateSendWindow","verifiedSendAsAddresses","markOutreachMailboxReconnectRequired","openOutreachMailboxClient","consumeOutreachOAuthState","mintOutreachOAuthState","outreachOidcNonce","outreachPkceVerifier","readOutreachOAuthState","recordOutreachOAuthState","OUTREACH_NOT_CONFIGURED_MESSAGE","OUTREACH_TEST_SENDS_PER_HOUR","OUTREACH_MAILBOX_ACTIVITY_TARGET","MAILBOX_ID","MAX_CODE_CHARS","noStore","ok","body","Response","json","status","headers","methodNotAllowed","allow","error","reason","Allow","readBody","request","catch","Array","isArray","notConfigured","readMailbox","id","data","activityName","mailbox","email","createOutreachMailboxRoutes","deps","loadManagedMailbox","who","gate","mailboxId","test","snapshot","firestore","orgId","get","exists","undefined","own","connectedByUid","uid","isOrgAdmin","availability","method","URL","url","searchParams","missing","config","readConfig","configured","push","stateSigningConfigured","redirectUri","length","canManageAll","connect","nowMs","now","state","claims","clientId","codeChallenge","nonce","loginHint","oauthCallback","params","read","plain","text","org","readOrg","slug","fragment","googleError","code","kind","Location","encodeURIComponent","connectComplete","aliases","consumed","grant","clientSecret","codeVerifier","transport","googleFailure","refreshToken","scope","identity","idToken","client","profileEmail","sendAsOptions","profile","sendAs","Promise","all","getProfile","listSendAs","emailAddress","reconnectRequired","some","option","displayName","isPrimary","isDefault","o","sub","mailboxDoc","credentialDoc","existingMailbox","existingCredential","current","mine","collection","doc","where","limit","size","defaultOption","find","reconnected","keepSendAs","provider","connectedAtMs","updatedAtMs","timezone","String","name","dailyCap","window","days","rampStartedAtMs","health","sentToday","sentOnDay","bounces","replies","lastSentAtMs","lastErrorAtMs","lastErrorCode","daily","createdAtMs","credential","providerAccountId","scopes","split","filter","Boolean","keyring","Number","batch","set","merge","commit","confirmAliasesByProvider","addresses","map","console","logOrgActivity","type","created","confirmedAliases","confirmed","settings","loaded","update","invalid","message","trim","toLowerCase","cap","running","Object","keys","next","autoPause","limited","consumeRateLimit","windowMs","allowed","named","to","opened","errorCode","sent","from","address","subject","join","sentTo","gmailMessageId","sentAtMs","disconnect","revocation","delete","readiness","expectation","readSenderReadiness","fresh","route","answer"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/mailboxes/mailbox-routes.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 { normalizeContactEmail } from '@aglyn/aglyn/app-utils/contacts'\nimport type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport {\n googleMailboxSenderExpectation,\n microsoftMailboxSenderExpectation,\n type SenderReadiness,\n type SenderReadinessExpectation,\n} from '@aglyn/shared-util-email'\nimport type { SecretBoxKeyring } from '@aglyn/shared-util-tools/secret-box'\nimport {\n OUTREACH_MAILBOX_PROVIDERS,\n type OutreachMailbox,\n type OutreachMailboxProvider,\n type OutreachMailboxStatus,\n type OutreachSendAsAddress,\n} from '../model/outreach.types'\nimport { createGmailClient } from '../transport/gmail-client'\nimport { createGraphClient, type GraphProfile } from '../transport/graph-client'\nimport { GmailTransportError, googleFailureAnswer } from '../transport/gmail-errors'\nimport {\n buildGoogleAuthorizationUrl,\n exchangeGoogleAuthorizationCode,\n grantedScopesInclude,\n OUTREACH_REQUIRED_GRANTED_SCOPES,\n pkceChallenge,\n readGoogleIdToken,\n type GoogleTokenResponse,\n} from '../transport/google-oauth'\nimport type { TransportDeps } from '../transport/http'\nimport {\n buildMicrosoftAuthorizationUrl,\n exchangeMicrosoftAuthorizationCode,\n microsoftScopesInclude,\n readMicrosoftIdToken,\n type MicrosoftTokenResponse,\n} from '../transport/microsoft-oauth'\nimport { Rfc5322MessageError } from '../transport/rfc5322'\nimport { sendOutreachMessage } from '../transport/send-message'\nimport {\n buildConnectReturnFragment,\n type OutreachApiRefusalReason,\n type OutreachConnectReturn,\n type OutreachMailboxAvailability,\n type OutreachMailboxAvailabilityGate,\n type OutreachMailboxDisconnectResponse,\n type OutreachMailboxReadinessResponse,\n} from './mailbox-api'\nimport {\n mailboxCredentialsRef,\n mailboxRef,\n outreachMailboxId,\n readMailboxCredentials,\n sealMailboxRefreshToken,\n type OutreachStoredMailboxCredentials,\n} from './mailbox-credentials'\nimport { revokeMailboxGrant } from './mailbox-revoke'\nimport {\n outreachMemberGate,\n refusal,\n type OutreachGateContext,\n type OutreachGateDeps,\n} from './mailbox-gate'\nimport {\n defaultSendAs,\n isValidTimezone,\n OUTREACH_DEFAULT_DAILY_CAP,\n OUTREACH_DEFAULT_WINDOW,\n OUTREACH_MAX_MAILBOXES_PER_MEMBER,\n validateDailyCap,\n validateDisplayName,\n validateSendWindow,\n verifiedSendAsAddresses,\n} from './mailbox-settings'\nimport {\n markOutreachMailboxReconnectRequired,\n openOutreachMailboxClient,\n} from './mailbox-transport'\nimport {\n consumeOutreachOAuthState,\n mintOutreachOAuthState,\n outreachOidcNonce,\n outreachPkceVerifier,\n readOutreachOAuthState,\n recordOutreachOAuthState,\n type OutreachOAuthStateClaims,\n} from './oauth-state'\nimport {\n outreachNotConfiguredMessage,\n readOutreachMicrosoftConfig,\n type OutreachGoogleConfigResult,\n type OutreachMicrosoftConfigResult,\n} from './outreach-config'\n\n/**\n * THE MAILBOX ROUTES (AGL-2978): connect a rep's Google or Microsoft 365\n * mailbox, keep its settings, pause it, test it, and disconnect it.\n *\n * A connect names its provider (AGL-3489). Both providers' consent screens\n * return to the one callback; the signed state says which one a connect went\n * to, and `connect/complete` redeems the code with that provider. Everything\n * after the provider has named the account — the mailbox limit, the mailbox\n * and its sealed grant, the activity row — is the same for both.\n *\n * ## The connect, end to end\n *\n * 1. `connect` (the member's session) mints a signed, single-use state bound\n * to the member and the organization, records its pending entry, and\n * answers Google's consent address — offline access, the consent prompt,\n * a PKCE challenge and an OpenID nonce, both derived from the state.\n * 2. Google redirects the browser to `oauth/callback`. That request carries\n * no bearer token, so it DOES nothing: it verifies the state's signature,\n * finds the organization's console path, and redirects to the Mailboxes\n * page with the code in the URL FRAGMENT, where no server and no `Referer`\n * sees it. Its release gate was asked about the organization and the\n * member the state names (the route's declared subject).\n * 3. The page takes the code out of the address bar and posts it to\n * `connect/complete` with the member's own session. That is where the\n * state is consumed, where it is refused if it was minted for anyone else,\n * and where the code is exchanged. So a consent screen someone else was\n * tricked into completing cannot connect THEIR mailbox to the member who\n * started it: the code arrives in the wrong browser, whose session is not\n * the state's member.\n *\n * ## Refusals, in order, on `connect/complete`\n *\n * A state that is invalid, minted for another member or organization,\n * expired, already used or superseded; a code Google refuses; a grant with no\n * refresh token; a grant missing `gmail.send` or `gmail.readonly` (the consent\n * screen lets a person untick them); an ID token that is not about this\n * exchange or whose address Google has not verified; an account with no Gmail\n * service (a Workspace user whose Gmail is off, or not yet provisioned) or one\n * Google bars from the Gmail API; a Gmail profile whose address is not the ID\n * token's; and a member at their mailbox limit. Each is a 4xx: only a Google\n * outage, or a request Google calls malformed, answers 502.\n *\n * A grant refused after the code exchange is dropped, not revoked. Google's\n * revocation ends the whole grant this client holds for the account, and the\n * same account may already be connected — by this member in another\n * organization, or by a teammate — so revoking a token this route merely\n * declines to keep could disconnect a mailbox that is working.\n *\n * ## Who may do what\n *\n * Every route but the callback climbs `outreachMemberGate`. The member who\n * connected a mailbox manages it; an organization owner or admin may also\n * change its settings, pause it and disconnect it — a departing rep's mailbox\n * has to be stoppable by somebody. Only its own member can send a test from\n * it, since the test lands in their inbox.\n */\n\nexport interface OutreachMailboxRouteDeps {\n firestore(): FirebaseFirestore.Firestore\n gate: OutreachGateDeps\n readConfig(): OutreachGoogleConfigResult\n /** The Microsoft app registration (AGL-3489); the environment's when omitted. */\n readMicrosoftConfig?(): OutreachMicrosoftConfigResult\n /** Whether the signing secret the OAuth state needs is configured. */\n stateSigningConfigured(): boolean\n redirectUri(requestUrl: string): string | null\n now(): number\n /** The network and retry policy every Google call uses. */\n transport: TransportDeps\n consumeRateLimit(key: string, options: { limit: number; windowMs: number }): Promise<{ allowed: boolean }>\n logOrgActivity(\n orgId: string,\n actor: { uid: string; email?: string | null },\n action: string,\n target: { type: typeof OUTREACH_MAILBOX_ACTIVITY_TARGET; id: string; name: string },\n ): Promise<void>\n confirmAliasesByProvider(\n firestore: FirebaseFirestore.Firestore,\n input: { orgId: string; uid: string; addresses: readonly string[]; nowMs: number },\n ): Promise<{ ok: boolean; confirmed?: string[] }>\n /** SPF, DKIM and DMARC for one sender, looked up and briefly remembered. */\n readSenderReadiness(expectation: SenderReadinessExpectation, options: { fresh?: boolean }): Promise<SenderReadiness>\n}\n\nexport interface OutreachMailboxRoutes {\n availability: PluginWebApiHandler\n connect: PluginWebApiHandler\n oauthCallback: PluginWebApiHandler\n connectComplete: PluginWebApiHandler\n settings: PluginWebApiHandler\n status: PluginWebApiHandler\n test: PluginWebApiHandler\n disconnect: PluginWebApiHandler\n readiness: PluginWebApiHandler\n}\n\n/** Test sends one mailbox may make in an hour. */\nexport const OUTREACH_TEST_SENDS_PER_HOUR = 5\n\n/**\n * The org activity target a mailbox's rows are filed under: Outreach's own\n * namespaced resource, which the feed reads as \"Mailbox\".\n */\nexport const OUTREACH_MAILBOX_ACTIVITY_TARGET = 'outreach:mailbox'\n\nconst MAILBOX_ID = /^[A-Za-z0-9_-]{1,128}$/\nconst MAX_CODE_CHARS = 2048\n\nconst noStore = { 'Cache-Control': 'no-store' }\n\nconst ok = (body: unknown) => Response.json(body, { status: 200, headers: noStore })\n\nfunction methodNotAllowed(allow: string): Response {\n return Response.json(\n { error: 'Method not allowed', reason: 'method-not-allowed' satisfies OutreachApiRefusalReason },\n { status: 405, headers: { ...noStore, Allow: allow } },\n )\n}\n\nasync function readBody(request: Request): Promise<Record<string, unknown>> {\n const body = await request.json().catch(() => null)\n return body && typeof body === 'object' && !Array.isArray(body) ? (body as Record<string, unknown>) : {}\n}\n\nfunction notConfigured(provider: OutreachMailboxProvider = 'google'): Response {\n return refusal(503, 'not-configured', outreachNotConfiguredMessage(provider))\n}\n\n/** The provider a request names; Google when it names none, `null` for one that is not a provider. */\nfunction readProvider(value: unknown): OutreachMailboxProvider | null {\n if (value === undefined || value === null || value === '') return 'google'\n return (OUTREACH_MAILBOX_PROVIDERS as readonly unknown[]).includes(value) ? (value as OutreachMailboxProvider) : null\n}\n\nconst providerName = (provider: OutreachMailboxProvider) => (provider === 'microsoft' ? 'Microsoft' : 'Google')\n\n/** What a provider's sign-in established about the account a connect is for. */\ninterface ConnectedAccount {\n provider: OutreachMailboxProvider\n providerAccountId: string\n email: string\n sendAsOptions: OutreachSendAsAddress[]\n scopes: string[]\n refreshToken: string\n keyring: SecretBoxKeyring\n}\n\n/** A mailbox document read defensively into the model shape. */\nfunction readMailbox(id: string, data: Record<string, unknown> | undefined): OutreachMailbox | null {\n if (!data) return null\n return { ...(data as unknown as OutreachMailbox), id }\n}\n\nconst activityName = (mailbox: Pick<OutreachMailbox, 'email'>) => mailbox.email\n\nexport function createOutreachMailboxRoutes(deps: OutreachMailboxRouteDeps): OutreachMailboxRoutes {\n const readMicrosoftConfig = () => (deps.readMicrosoftConfig ?? readOutreachMicrosoftConfig)()\n /** Whether a provider's client and the token key are configured here. */\n const providerConfigured = (provider: OutreachMailboxProvider) =>\n provider === 'microsoft' ? readMicrosoftConfig().configured : deps.readConfig().configured\n\n /** The gate, then the mailbox the body names, then who may touch it. */\n async function loadManagedMailbox(\n request: Request,\n body: Record<string, unknown>,\n who: 'member-or-admin' | 'member',\n ): Promise<Response | { gate: OutreachGateContext; mailbox: OutreachMailbox }> {\n const gate = await outreachMemberGate(request, body['orgId'], deps.gate)\n if (gate instanceof Response) return gate\n const mailboxId = typeof body['mailboxId'] === 'string' ? body['mailboxId'] : ''\n if (!MAILBOX_ID.test(mailboxId)) return refusal(400, 'invalid-request', 'Name the mailbox.')\n const snapshot = await mailboxRef(deps.firestore(), gate.orgId, mailboxId).get()\n const mailbox = readMailbox(mailboxId, snapshot.exists ? snapshot.data() : undefined)\n if (!mailbox) return refusal(404, 'mailbox-not-found', 'That mailbox is not connected to this organization.')\n const own = mailbox.connectedByUid === gate.uid\n if (!own && !(who === 'member-or-admin' && gate.isOrgAdmin)) {\n return refusal(\n 403,\n 'not-your-mailbox',\n who === 'member'\n ? 'Only the member who connected this mailbox can do that.'\n : 'Only the member who connected this mailbox, or an organization owner or admin, can change it.',\n )\n }\n return { gate, mailbox }\n }\n\n const availability: PluginWebApiHandler = async (request) => {\n if (request.method !== 'GET') return methodNotAllowed('GET')\n const gate = await outreachMemberGate(request, new URL(request.url).searchParams.get('orgId'), deps.gate)\n if (gate instanceof Response) return gate\n // Every gate is named, not the first: an operator fixing a deployment\n // should see the whole list once rather than one variable per redeploy.\n const missing: OutreachMailboxAvailabilityGate[] = []\n const config = deps.readConfig()\n const microsoft = readMicrosoftConfig()\n if (config.configured === false) missing.push({ gate: 'google', missing: config.missing })\n if (microsoft.configured === false) missing.push({ gate: 'microsoft', missing: microsoft.missing })\n const shared = deps.stateSigningConfigured() && Boolean(deps.redirectUri(request.url))\n if (!deps.stateSigningConfigured()) missing.push({ gate: 'state' })\n if (!deps.redirectUri(request.url)) missing.push({ gate: 'redirect' })\n const providers = { google: shared && config.configured, microsoft: shared && microsoft.configured }\n return ok({\n // Google's readiness, as this field has always meant; `providers` says each.\n configured: providers.google,\n providers,\n canManageAll: gate.isOrgAdmin,\n ...(missing.length ? { missing } : {}),\n } satisfies OutreachMailboxAvailability)\n }\n\n const connect: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return methodNotAllowed('POST')\n const body = await readBody(request)\n const gate = await outreachMemberGate(request, body['orgId'], deps.gate)\n if (gate instanceof Response) return gate\n const provider = readProvider(body['provider'])\n if (!provider) return refusal(400, 'invalid-request', 'Choose Google or Microsoft.')\n const redirectUri = deps.redirectUri(request.url)\n if (!redirectUri || !deps.stateSigningConfigured()) return notConfigured(provider)\n\n if (provider === 'microsoft') {\n const microsoft = readMicrosoftConfig()\n if (!microsoft.configured) return notConfigured(provider)\n const nowMs = deps.now()\n const { state, claims } = mintOutreachOAuthState({ orgId: gate.orgId, uid: gate.uid, nowMs, provider })\n await recordOutreachOAuthState(deps.firestore(), { claims, redirectUri, nowMs })\n const url = buildMicrosoftAuthorizationUrl({\n clientId: microsoft.config.clientId,\n tenant: microsoft.config.tenant,\n redirectUri,\n state,\n codeChallenge: pkceChallenge(outreachPkceVerifier(claims.nonce)),\n nonce: outreachOidcNonce(claims.nonce),\n loginHint: typeof body['loginHint'] === 'string' ? normalizeContactEmail(body['loginHint']) : null,\n })\n return ok({ url })\n }\n\n const config = deps.readConfig()\n if (!config.configured) return notConfigured()\n const nowMs = deps.now()\n const { state, claims } = mintOutreachOAuthState({ orgId: gate.orgId, uid: gate.uid, nowMs })\n await recordOutreachOAuthState(deps.firestore(), { claims, redirectUri, nowMs })\n const url = buildGoogleAuthorizationUrl({\n clientId: config.config.clientId,\n redirectUri,\n state,\n codeChallenge: pkceChallenge(outreachPkceVerifier(claims.nonce)),\n nonce: outreachOidcNonce(claims.nonce),\n loginHint: gate.email,\n })\n return ok({ url })\n }\n\n const oauthCallback: PluginWebApiHandler = async (request) => {\n if (request.method !== 'GET') return methodNotAllowed('GET')\n const params = new URL(request.url).searchParams\n const read = readOutreachOAuthState(params.get('state'), deps.now())\n const plain = (status: number, text: string) =>\n new Response(text, {\n status,\n headers: { ...noStore, 'Content-Type': 'text/plain; charset=utf-8', 'Referrer-Policy': 'no-referrer' },\n })\n if (read.ok === false && read.refusal === 'state-invalid') {\n return plain(400, 'This connection link is not valid. Return to the console and connect the mailbox again.')\n }\n const claims = read.ok ? read.claims : (read as { claims: OutreachOAuthStateClaims }).claims\n const org = await deps.gate.readOrg(claims.orgId)\n const slug = typeof org?.['slug'] === 'string' ? org['slug'] : ''\n if (!slug) return plain(404, 'That organization could not be found.')\n\n let fragment: OutreachConnectReturn\n const providerError = params.get('error')\n const code = params.get('code') ?? ''\n // An error fragment names a Microsoft connect, so the page says whose screen it was.\n const named = claims.provider === 'microsoft' ? { provider: claims.provider } : {}\n if (providerError) {\n fragment = { kind: 'error', reason: providerError === 'access_denied' ? 'access_denied' : 'google_error', ...named }\n } else if (!read.ok) {\n fragment = { kind: 'error', reason: 'expired', ...named }\n } else if (!code || code.length > MAX_CODE_CHARS) {\n fragment = { kind: 'error', reason: 'google_error', ...named }\n } else {\n fragment = { kind: 'code', code, state: params.get('state') ?? '' }\n }\n // A RELATIVE location on the callback's own origin: the path is built from\n // the organization's slug, never from anything the request supplied.\n return new Response(null, {\n status: 303,\n headers: {\n ...noStore,\n 'Referrer-Policy': 'no-referrer',\n Location: `/${encodeURIComponent(slug)}/outreach/mailboxes#${buildConnectReturnFragment(fragment)}`,\n },\n })\n }\n\n const connectComplete: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return methodNotAllowed('POST')\n const body = await readBody(request)\n const gate = await outreachMemberGate(request, body['orgId'], deps.gate)\n if (gate instanceof Response) return gate\n if (!deps.stateSigningConfigured()) return notConfigured()\n if (!deps.readConfig().configured && !readMicrosoftConfig().configured) return notConfigured()\n const code = typeof body['code'] === 'string' ? body['code'] : ''\n if (!code || code.length > MAX_CODE_CHARS) return refusal(400, 'invalid-request', 'The connection did not return a code.')\n\n const firestore = deps.firestore()\n const nowMs = deps.now()\n const read = readOutreachOAuthState(body['state'], nowMs)\n if (read.ok === false && read.refusal === 'state-invalid') {\n return refusal(400, 'state-invalid', 'This connection could not be verified. Connect the mailbox again.')\n }\n const claims = read.ok ? read.claims : (read as { claims: typeof read.claims }).claims\n if (claims.uid !== gate.uid) {\n return refusal(\n 403,\n 'state-user-mismatch',\n 'This connection was started by another member. Sign in as that member, or connect the mailbox again.',\n )\n }\n if (claims.orgId !== gate.orgId) {\n return refusal(400, 'state-org-mismatch', 'This connection was started in another organization.')\n }\n if (!read.ok) return refusal(410, 'state-expired', 'This connection took too long. Connect the mailbox again.')\n if (!providerConfigured(claims.provider)) return notConfigured(claims.provider)\n\n const consumed = await consumeOutreachOAuthState(firestore, { claims, nowMs })\n if (consumed.ok === false) {\n if (consumed.refusal === 'state-expired') {\n return refusal(410, 'state-expired', 'This connection took too long. Connect the mailbox again.')\n }\n return refusal(\n 409,\n consumed.refusal,\n consumed.refusal === 'state-superseded'\n ? 'A newer connection was started. Finish that one, or connect the mailbox again.'\n : 'This connection was already used. Connect the mailbox again.',\n )\n }\n\n const account =\n claims.provider === 'microsoft'\n ? await redeemMicrosoftCode({ code, claims, redirectUri: consumed.redirectUri, nowMs })\n : await redeemGoogleCode({ code, claims, redirectUri: consumed.redirectUri, nowMs })\n if (account instanceof Response) return account\n return storeConnectedMailbox({ gate, body, account, nowMs })\n }\n\n /** Google's half of a connect: the code redeemed, the grant and the account checked. */\n async function redeemGoogleCode(input: {\n code: string\n claims: OutreachOAuthStateClaims\n redirectUri: string\n nowMs: number\n }): Promise<Response | ConnectedAccount> {\n const { code, claims, nowMs } = input\n const config = deps.readConfig()\n if (!config.configured) return notConfigured()\n let grant: GoogleTokenResponse\n try {\n grant = await exchangeGoogleAuthorizationCode(\n {\n clientId: config.config.clientId,\n clientSecret: config.config.clientSecret,\n code,\n redirectUri: input.redirectUri,\n codeVerifier: outreachPkceVerifier(claims.nonce),\n },\n deps.transport,\n )\n } catch (error) {\n if (error instanceof GmailTransportError && error.code === 'invalid_grant') {\n return refusal(400, 'code-rejected', \"Google's sign-in expired or was already used. Connect the mailbox again.\")\n }\n if (error instanceof GmailTransportError && error.code === 'client_misconfigured') {\n return refusal(503, 'not-configured', \"Google refused this deployment's OAuth client.\")\n }\n if (error instanceof GmailTransportError && error.code === 'insufficient_scope') {\n return refusal(\n 422,\n 'scopes-missing',\n 'Sequences needs permission to send and to read your mail. Connect again and allow both.',\n )\n }\n return googleFailure(error, 'connect/complete token exchange')\n }\n if (!grant.refreshToken) {\n return refusal(\n 422,\n 'refresh-token-missing',\n 'Google did not grant offline access, so the mailbox could not send later. Connect it again.',\n )\n }\n if (!grantedScopesInclude(grant.scope, OUTREACH_REQUIRED_GRANTED_SCOPES)) {\n return refusal(\n 422,\n 'scopes-missing',\n 'Sequences needs permission to send and to read your mail. Connect again and allow both.',\n )\n }\n const identity = readGoogleIdToken(grant.idToken, {\n clientId: config.config.clientId,\n nonce: outreachOidcNonce(claims.nonce),\n nowMs,\n })\n if (identity.ok === false) {\n return refusal(\n 422,\n 'identity-unverified',\n identity.refusal === 'email-unverified'\n ? \"Google has not verified this account's email address.\"\n : \"Google's answer could not be verified. Connect the mailbox again.\",\n )\n }\n\n const client = createGmailClient({\n ...deps.transport,\n clientId: config.config.clientId,\n clientSecret: config.config.clientSecret,\n refreshToken: grant.refreshToken,\n })\n let profileEmail: string\n let sendAsOptions: ReturnType<typeof verifiedSendAsAddresses>\n try {\n const [profile, sendAs] = await Promise.all([client.getProfile(), client.listSendAs()])\n profileEmail = profile.emailAddress\n sendAsOptions = verifiedSendAsAddresses(sendAs)\n } catch (error) {\n if (error instanceof GmailTransportError && error.reconnectRequired) {\n return refusal(\n 422,\n 'scopes-missing',\n 'Sequences needs permission to send and to read your mail. Connect again and allow both.',\n )\n }\n return googleFailure(error, 'connect/complete Gmail profile')\n }\n if (profileEmail !== identity.identity.email) {\n return refusal(422, 'account-mismatch', 'The Gmail account and the Google sign-in did not match. Connect again.')\n }\n if (!sendAsOptions.some((option) => option.email === profileEmail)) {\n sendAsOptions = [\n { email: profileEmail, displayName: '', isPrimary: true, isDefault: !sendAsOptions.some((o) => o.isDefault) },\n ...sendAsOptions,\n ]\n }\n\n return {\n provider: 'google',\n providerAccountId: identity.identity.sub,\n email: profileEmail,\n sendAsOptions,\n scopes: grant.scope.split(/\\s+/).filter(Boolean),\n refreshToken: grant.refreshToken,\n keyring: config.config.keyring,\n }\n }\n\n /**\n * Microsoft's half of a connect (AGL-3489): the code redeemed, the grant's\n * permissions checked, the ID token read, and the account read back from\n * Graph — the same account the sign-in named, by its directory id.\n */\n async function redeemMicrosoftCode(input: {\n code: string\n claims: OutreachOAuthStateClaims\n redirectUri: string\n nowMs: number\n }): Promise<Response | ConnectedAccount> {\n const { code, claims, nowMs } = input\n const config = readMicrosoftConfig()\n if (!config.configured) return notConfigured('microsoft')\n const scopesMissing = () =>\n refusal(\n 422,\n 'scopes-missing',\n 'Sequences needs permission to send and to read your mail. Connect again and allow both.',\n )\n let grant: MicrosoftTokenResponse\n try {\n grant = await exchangeMicrosoftAuthorizationCode(\n {\n clientId: config.config.clientId,\n clientSecret: config.config.clientSecret,\n tenant: config.config.tenant,\n code,\n redirectUri: input.redirectUri,\n codeVerifier: outreachPkceVerifier(claims.nonce),\n },\n deps.transport,\n )\n } catch (error) {\n if (error instanceof GmailTransportError && error.code === 'invalid_grant') {\n return refusal(400, 'code-rejected', \"Microsoft's sign-in expired or was already used. Connect the mailbox again.\")\n }\n if (error instanceof GmailTransportError && error.code === 'client_misconfigured') {\n return refusal(503, 'not-configured', \"Microsoft refused this deployment's app registration.\")\n }\n if (error instanceof GmailTransportError && error.code === 'insufficient_scope') return scopesMissing()\n return googleFailure(error, 'connect/complete Microsoft token exchange')\n }\n if (!grant.refreshToken) {\n return refusal(\n 422,\n 'refresh-token-missing',\n 'Microsoft did not grant offline access, so the mailbox could not send later. Connect it again.',\n )\n }\n if (!microsoftScopesInclude(grant.scope)) return scopesMissing()\n const identity = readMicrosoftIdToken(grant.idToken, {\n clientId: config.config.clientId,\n nonce: outreachOidcNonce(claims.nonce),\n nowMs,\n })\n if (identity.ok === false) {\n return refusal(422, 'identity-unverified', \"Microsoft's answer could not be verified. Connect the mailbox again.\")\n }\n const client = createGraphClient({\n ...deps.transport,\n clientId: config.config.clientId,\n clientSecret: config.config.clientSecret,\n tenant: config.config.tenant,\n refreshToken: grant.refreshToken,\n accessToken: { token: grant.accessToken, expiresInSeconds: grant.expiresInSeconds },\n })\n let profile: GraphProfile\n try {\n profile = await client.getProfile()\n } catch (error) {\n if (error instanceof GmailTransportError && error.reconnectRequired) return scopesMissing()\n return googleFailure(error, 'connect/complete Microsoft profile')\n }\n if (profile.id !== identity.identity.oid || !profile.emailAddress) {\n return refusal(\n 422,\n 'account-mismatch',\n 'The Microsoft 365 mailbox and the Microsoft sign-in did not match. Connect again.',\n )\n }\n return {\n provider: 'microsoft',\n providerAccountId: identity.identity.sub,\n email: profile.emailAddress,\n sendAsOptions: [\n { email: profile.emailAddress, displayName: profile.displayName, isPrimary: true, isDefault: true },\n ],\n scopes: grant.scope.split(/\\s+/).filter(Boolean),\n refreshToken: grant.refreshToken,\n keyring: config.config.keyring,\n }\n }\n\n /**\n * The connect's end, whichever provider named the account: the member's\n * mailbox limit, the mailbox (new, or a reconnect that keeps its\n * settings), its sealed grant, the aliases the provider confirmed, and the\n * activity row.\n */\n async function storeConnectedMailbox(input: {\n gate: OutreachGateContext\n body: Record<string, unknown>\n account: ConnectedAccount\n nowMs: number\n }): Promise<Response> {\n const { gate, body, account, nowMs } = input\n const { provider, email: profileEmail, sendAsOptions } = account\n const firestore = deps.firestore()\n const mailboxId = outreachMailboxId(gate.orgId, gate.uid, account.providerAccountId, provider)\n const mailboxDoc = mailboxRef(firestore, gate.orgId, mailboxId)\n const credentialDoc = mailboxCredentialsRef(firestore, mailboxId)\n const [existingMailbox, existingCredential] = await Promise.all([mailboxDoc.get(), credentialDoc.get()])\n const current = readMailbox(mailboxId, existingMailbox.exists ? existingMailbox.data() : undefined)\n if (!current) {\n const mine = await firestore\n .collection('orgs')\n .doc(gate.orgId)\n .collection('outreachMailboxes')\n .where('connectedByUid', '==', gate.uid)\n .limit(OUTREACH_MAX_MAILBOXES_PER_MEMBER)\n .get()\n if (mine.size >= OUTREACH_MAX_MAILBOXES_PER_MEMBER) {\n return refusal(\n 409,\n 'mailbox-limit',\n `You can connect up to ${OUTREACH_MAX_MAILBOXES_PER_MEMBER} mailboxes. Disconnect one before connecting another.`,\n )\n }\n }\n\n const defaultOption = sendAsOptions.find((option) => option.email === defaultSendAs(sendAsOptions, profileEmail))\n let mailbox: OutreachMailbox\n /** On a reconnect, only the fields a connect decides — see the write below. */\n let reconnected: Partial<OutreachMailbox> | null = null\n if (current) {\n const keepSendAs = sendAsOptions.some((option) => option.email === current.sendAs)\n reconnected = {\n provider,\n email: profileEmail,\n sendAs: keepSendAs ? current.sendAs : (defaultOption?.email ?? profileEmail),\n sendAsOptions,\n // A reconnect restores a mailbox the provider had cut off, and leaves a\n // mailbox its member paused paused.\n status: (current.status === 'paused' ? 'paused' : 'connected') satisfies OutreachMailboxStatus,\n connectedAtMs: nowMs,\n updatedAtMs: nowMs,\n }\n mailbox = { ...current, ...reconnected }\n } else {\n const timezone = isValidTimezone(body['timezone']) ? String(body['timezone']) : 'UTC'\n const displayName = validateDisplayName(defaultOption?.displayName || gate.name || '')\n mailbox = {\n id: mailboxId,\n provider,\n email: profileEmail,\n sendAs: defaultOption?.email ?? profileEmail,\n sendAsOptions,\n displayName: typeof displayName === 'string' ? displayName : '',\n status: 'connected',\n dailyCap: OUTREACH_DEFAULT_DAILY_CAP,\n window: { ...OUTREACH_DEFAULT_WINDOW, days: [...OUTREACH_DEFAULT_WINDOW.days] },\n timezone,\n rampStartedAtMs: nowMs,\n health: {\n sentToday: 0,\n sentOnDay: null,\n bounces: 0,\n replies: 0,\n lastSentAtMs: null,\n lastErrorAtMs: null,\n lastErrorCode: null,\n daily: {},\n },\n connectedByUid: gate.uid,\n connectedAtMs: nowMs,\n createdAtMs: nowMs,\n updatedAtMs: nowMs,\n }\n }\n const credential: OutreachStoredMailboxCredentials = {\n id: mailboxId,\n orgId: gate.orgId,\n mailboxId,\n provider,\n connectedByUid: gate.uid,\n providerAccountId: account.providerAccountId,\n email: profileEmail,\n scopes: account.scopes,\n ...sealMailboxRefreshToken(account.refreshToken, mailboxId, account.keyring),\n createdAtMs: Number(existingCredential.exists ? existingCredential.get('createdAtMs') : 0) || nowMs,\n updatedAtMs: nowMs,\n }\n const batch = firestore.batch()\n // A reconnect MERGES only what a connect decides, so the settings a member\n // chose and the health counters the sending runtime increments are never\n // written back from a read that may already be stale.\n if (reconnected) batch.set(mailboxDoc, reconnected, { merge: true })\n else batch.set(mailboxDoc, mailbox)\n batch.set(credentialDoc, credential)\n await batch.commit()\n\n const aliases: { ok: boolean; confirmed?: string[] } = await deps\n .confirmAliasesByProvider(firestore, {\n orgId: gate.orgId,\n uid: gate.uid,\n addresses: sendAsOptions.map((option) => option.email),\n nowMs,\n })\n .catch((error: unknown) => {\n console.error('[outreach] confirming member aliases from send-as failed', error)\n return { ok: false }\n })\n await deps.logOrgActivity(\n gate.orgId,\n { uid: gate.uid, email: gate.email },\n current ? 'Reconnected a mailbox in Sequences' : 'Connected a mailbox in Sequences',\n { type: OUTREACH_MAILBOX_ACTIVITY_TARGET, id: mailboxId, name: activityName(mailbox) },\n )\n return ok({\n ok: true,\n mailbox,\n created: !current,\n confirmedAliases: aliases.ok ? (aliases.confirmed ?? []) : [],\n })\n }\n\n const settings: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return methodNotAllowed('POST')\n const body = await readBody(request)\n const loaded = await loadManagedMailbox(request, body, 'member-or-admin')\n if (loaded instanceof Response) return loaded\n const { gate, mailbox } = loaded\n const update: Partial<OutreachMailbox> = {}\n const invalid = (message: string) => refusal(400, 'invalid-settings', message)\n\n if (body['sendAs'] !== undefined) {\n const sendAs = typeof body['sendAs'] === 'string' ? body['sendAs'].trim().toLowerCase() : ''\n if (!(mailbox.sendAsOptions ?? []).some((option) => option.email === sendAs)) {\n return invalid('Choose one of the addresses the provider has verified for this account.')\n }\n update.sendAs = sendAs\n }\n if (body['displayName'] !== undefined) {\n const name = validateDisplayName(body['displayName'])\n if (typeof name !== 'string') return invalid(name.message)\n update.displayName = name\n }\n if (body['dailyCap'] !== undefined) {\n const cap = validateDailyCap(body['dailyCap'])\n if (typeof cap !== 'number') return invalid(cap.message)\n update.dailyCap = cap\n }\n if (body['window'] !== undefined) {\n const window = validateSendWindow(body['window'])\n if (!('days' in window)) return invalid(window.message)\n update.window = window\n }\n if (body['timezone'] !== undefined) {\n if (!isValidTimezone(body['timezone'])) return invalid('Choose a timezone from the list.')\n update.timezone = body['timezone']\n }\n if (body['warmUp'] !== undefined) {\n /*\n * The ramp guards a NEW sender's reputation; a mailbox that has sent\n * mail for years has one already, and ramping it to ten a day guards\n * nothing (AGL-3228). Its member decides. Turning the ramp back on\n * starts a fresh one from today, which is what \"warm up\" means.\n */\n if (typeof body['warmUp'] !== 'boolean') return invalid('Say whether to warm the mailbox up gradually.')\n const running = typeof mailbox.rampStartedAtMs === 'number'\n if (body['warmUp'] && !running) update.rampStartedAtMs = deps.now()\n if (!body['warmUp'] && running) update.rampStartedAtMs = null\n }\n if (!Object.keys(update).length) return ok({ ok: true, mailbox })\n const nowMs = deps.now()\n await mailboxRef(deps.firestore(), gate.orgId, mailbox.id).update({ ...update, updatedAtMs: nowMs })\n return ok({ ok: true, mailbox: { ...mailbox, ...update, updatedAtMs: nowMs } })\n }\n\n const status: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return methodNotAllowed('POST')\n const body = await readBody(request)\n if (typeof body['paused'] !== 'boolean') return refusal(400, 'invalid-request', 'Say whether to pause the mailbox.')\n const loaded = await loadManagedMailbox(request, body, 'member-or-admin')\n if (loaded instanceof Response) return loaded\n const { gate, mailbox } = loaded\n if (mailbox.status === 'reconnect_required' || mailbox.status === 'disconnected') {\n return refusal(409, 'reconnect-required', 'Reconnect this mailbox before pausing or resuming it.')\n }\n const next: OutreachMailboxStatus = body['paused'] ? 'paused' : 'connected'\n if (mailbox.status === next) return ok({ ok: true, mailbox })\n const nowMs = deps.now()\n // A member's pause or resume answers an automatic pause (AGL-2981): its\n // reason goes, and the next bounce is judged as new evidence.\n await mailboxRef(deps.firestore(), gate.orgId, mailbox.id).update({\n status: next,\n autoPause: null,\n updatedAtMs: nowMs,\n })\n await deps.logOrgActivity(\n gate.orgId,\n { uid: gate.uid, email: gate.email },\n next === 'paused' ? 'Paused a mailbox in Sequences' : 'Resumed a mailbox in Sequences',\n { type: OUTREACH_MAILBOX_ACTIVITY_TARGET, id: mailbox.id, name: activityName(mailbox) },\n )\n return ok({ ok: true, mailbox: { ...mailbox, status: next, autoPause: null, updatedAtMs: nowMs } })\n }\n\n const test: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return methodNotAllowed('POST')\n const body = await readBody(request)\n const loaded = await loadManagedMailbox(request, body, 'member')\n if (loaded instanceof Response) return loaded\n const { gate, mailbox } = loaded\n if (mailbox.status === 'reconnect_required' || mailbox.status === 'disconnected') {\n return refusal(409, 'reconnect-required', 'Reconnect this mailbox before sending from it.')\n }\n const provider: OutreachMailboxProvider = mailbox.provider === 'microsoft' ? 'microsoft' : 'google'\n if (!providerConfigured(provider)) return notConfigured(provider)\n const limited = await deps.consumeRateLimit(`outreach-mailbox-test:${mailbox.id}`, {\n limit: OUTREACH_TEST_SENDS_PER_HOUR,\n windowMs: 60 * 60 * 1000,\n })\n if (!limited.allowed) {\n return refusal(429, 'rate-limited', `A mailbox can send ${OUTREACH_TEST_SENDS_PER_HOUR} tests an hour. Try again later.`)\n }\n /*\n * To the member's own address unless they name another (AGL-3228). A\n * test to oneself never leaves the provider, so it arrives with no\n * Authentication-Results at all and cannot show whether the send-as\n * domain's DKIM and DMARC hold; an outside mailbox the member can read\n * is the only place that can be seen. The rate limit above is the\n * whole guard on who may be written to — it is the member's own\n * account sending, as it could from its own compose window.\n */\n const named = body['to']\n const to =\n named === undefined || named === null || String(named).trim() === ''\n ? mailbox.email\n : normalizeContactEmail(String(named))\n if (!to) {\n return refusal(400, 'invalid-request', `\"${String(named).trim()}\" isn't a valid email address.`)\n }\n const firestore = deps.firestore()\n const nowMs = deps.now()\n const opened = await openOutreachMailboxClient(\n firestore,\n { mailboxId: mailbox.id },\n { ...deps.transport, readConfig: deps.readConfig, readMicrosoftConfig, now: deps.now },\n )\n if (opened.ok === false) {\n if (opened.reason === 'not-configured') return notConfigured(provider)\n await markOutreachMailboxReconnectRequired(firestore, {\n orgId: gate.orgId,\n mailboxId: mailbox.id,\n errorCode: opened.reason,\n nowMs,\n })\n return refusal(409, 'reconnect-required', 'This mailbox needs to be connected again before it can send.')\n }\n try {\n const sent = await sendOutreachMessage(opened.client, {\n from: { address: mailbox.sendAs, name: mailbox.displayName },\n to,\n subject: 'Sequences test message',\n text: [\n 'This is a test message from Sequences.',\n '',\n `It was sent through the connected mailbox ${mailbox.email} as ${mailbox.sendAs}. ` +\n 'If it reached the inbox, this mailbox can send.',\n ...(to === mailbox.email\n ? []\n : [\n '',\n 'To read whether the sender is authenticated, open this message\\u2019s original source ' +\n 'and look for dkim=pass, spf=pass and dmarc=pass in Authentication-Results.',\n ]),\n ].join('\\n'),\n })\n return ok({ ok: true, sentTo: to, gmailMessageId: sent.gmailMessageId, sentAtMs: nowMs })\n } catch (error) {\n if (error instanceof GmailTransportError && error.reconnectRequired) {\n await markOutreachMailboxReconnectRequired(firestore, {\n orgId: gate.orgId,\n mailboxId: mailbox.id,\n errorCode: error.code,\n nowMs,\n })\n return refusal(\n 409,\n 'reconnect-required',\n `${providerName(provider)} refused this mailbox’s access. Connect it again.`,\n )\n }\n if (error instanceof Rfc5322MessageError) {\n return refusal(400, 'invalid-settings', `The test could not be written: ${error.message}`)\n }\n return googleFailure(error, 'test send')\n }\n }\n\n const disconnect: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return methodNotAllowed('POST')\n const body = await readBody(request)\n const loaded = await loadManagedMailbox(request, body, 'member-or-admin')\n if (loaded instanceof Response) return loaded\n const { gate, mailbox } = loaded\n const firestore = deps.firestore()\n const credentialDoc = mailboxCredentialsRef(firestore, mailbox.id)\n const snapshot = await credentialDoc.get()\n const credential = readMailboxCredentials(snapshot.exists ? snapshot.data() : null)\n const revocation = credential\n ? await revokeMailboxGrant(firestore, credential, deps)\n : ('already-invalid' as const)\n const batch = firestore.batch()\n batch.delete(credentialDoc)\n batch.delete(mailboxRef(firestore, gate.orgId, mailbox.id))\n await batch.commit()\n await deps.logOrgActivity(\n gate.orgId,\n { uid: gate.uid, email: gate.email },\n 'Disconnected a mailbox in Sequences',\n { type: OUTREACH_MAILBOX_ACTIVITY_TARGET, id: mailbox.id, name: activityName(mailbox) },\n )\n return ok({ ok: true, revocation } satisfies OutreachMailboxDisconnectResponse)\n }\n\n /**\n * How receivers judge mail from the address a mailbox sends as. Any member\n * who can see the mailbox can read it: it is a reading of public DNS about\n * the organization's own domain, and changes nothing.\n */\n const readiness: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return methodNotAllowed('POST')\n const body = await readBody(request)\n const gate = await outreachMemberGate(request, body['orgId'], deps.gate)\n if (gate instanceof Response) return gate\n const mailboxId = typeof body['mailboxId'] === 'string' ? body['mailboxId'] : ''\n if (!MAILBOX_ID.test(mailboxId)) return refusal(400, 'invalid-request', 'Name the mailbox.')\n const snapshot = await mailboxRef(deps.firestore(), gate.orgId, mailboxId).get()\n const mailbox = readMailbox(mailboxId, snapshot.exists ? snapshot.data() : undefined)\n if (!mailbox) return refusal(404, 'mailbox-not-found', 'That mailbox is not connected to this organization.')\n const sendAs = mailbox.sendAs || mailbox.email\n const expectation =\n mailbox.provider === 'microsoft'\n ? microsoftMailboxSenderExpectation(sendAs)\n : googleMailboxSenderExpectation(sendAs)\n const read = expectation\n ? await deps.readSenderReadiness(expectation, { fresh: body['fresh'] === true }).catch(() => null)\n : null\n return ok({ ok: true, sendAs, readiness: read } satisfies OutreachMailboxReadinessResponse)\n }\n\n return { availability, connect, oauthCallback, connectComplete, settings, status, test, disconnect, readiness }\n}\n\n/** A Google failure the route could not handle itself, as the refusal {@link googleFailureAnswer} decides. */\nfunction googleFailure(error: unknown, route: string): Response {\n const answer = googleFailureAnswer(error, route)\n return refusal(answer.status, answer.reason, answer.error)\n}\n"],"names":["normalizeContactEmail","googleMailboxSenderExpectation","microsoftMailboxSenderExpectation","OUTREACH_MAILBOX_PROVIDERS","createGmailClient","createGraphClient","GmailTransportError","googleFailureAnswer","buildGoogleAuthorizationUrl","exchangeGoogleAuthorizationCode","grantedScopesInclude","OUTREACH_REQUIRED_GRANTED_SCOPES","pkceChallenge","readGoogleIdToken","buildMicrosoftAuthorizationUrl","exchangeMicrosoftAuthorizationCode","microsoftScopesInclude","readMicrosoftIdToken","Rfc5322MessageError","sendOutreachMessage","buildConnectReturnFragment","mailboxCredentialsRef","mailboxRef","outreachMailboxId","readMailboxCredentials","sealMailboxRefreshToken","revokeMailboxGrant","outreachMemberGate","refusal","defaultSendAs","isValidTimezone","OUTREACH_DEFAULT_DAILY_CAP","OUTREACH_DEFAULT_WINDOW","OUTREACH_MAX_MAILBOXES_PER_MEMBER","validateDailyCap","validateDisplayName","validateSendWindow","verifiedSendAsAddresses","markOutreachMailboxReconnectRequired","openOutreachMailboxClient","consumeOutreachOAuthState","mintOutreachOAuthState","outreachOidcNonce","outreachPkceVerifier","readOutreachOAuthState","recordOutreachOAuthState","outreachNotConfiguredMessage","readOutreachMicrosoftConfig","OUTREACH_TEST_SENDS_PER_HOUR","OUTREACH_MAILBOX_ACTIVITY_TARGET","MAILBOX_ID","MAX_CODE_CHARS","noStore","ok","body","Response","json","status","headers","methodNotAllowed","allow","error","reason","Allow","readBody","request","catch","Array","isArray","notConfigured","provider","readProvider","value","undefined","includes","providerName","readMailbox","id","data","activityName","mailbox","email","createOutreachMailboxRoutes","deps","readMicrosoftConfig","providerConfigured","configured","readConfig","loadManagedMailbox","who","gate","mailboxId","test","snapshot","firestore","orgId","get","exists","own","connectedByUid","uid","isOrgAdmin","availability","method","URL","url","searchParams","missing","config","microsoft","push","shared","stateSigningConfigured","Boolean","redirectUri","providers","google","canManageAll","length","connect","nowMs","now","state","claims","clientId","tenant","codeChallenge","nonce","loginHint","oauthCallback","params","read","plain","text","org","readOrg","slug","fragment","providerError","code","named","kind","Location","encodeURIComponent","connectComplete","consumed","account","redeemMicrosoftCode","redeemGoogleCode","storeConnectedMailbox","input","grant","clientSecret","codeVerifier","transport","googleFailure","refreshToken","scope","identity","idToken","client","profileEmail","sendAsOptions","profile","sendAs","Promise","all","getProfile","listSendAs","emailAddress","reconnectRequired","some","option","displayName","isPrimary","isDefault","o","providerAccountId","sub","scopes","split","filter","keyring","scopesMissing","accessToken","token","expiresInSeconds","oid","aliases","mailboxDoc","credentialDoc","existingMailbox","existingCredential","current","mine","collection","doc","where","limit","size","defaultOption","find","reconnected","keepSendAs","connectedAtMs","updatedAtMs","timezone","String","name","dailyCap","window","days","rampStartedAtMs","health","sentToday","sentOnDay","bounces","replies","lastSentAtMs","lastErrorAtMs","lastErrorCode","daily","createdAtMs","credential","Number","batch","set","merge","commit","confirmAliasesByProvider","addresses","map","console","logOrgActivity","type","created","confirmedAliases","confirmed","settings","loaded","update","invalid","message","trim","toLowerCase","cap","running","Object","keys","next","autoPause","limited","consumeRateLimit","windowMs","allowed","to","opened","errorCode","sent","from","address","subject","join","sentTo","gmailMessageId","sentAtMs","disconnect","revocation","delete","readiness","expectation","readSenderReadiness","fresh","route","answer"],"mappings":"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"}
|
|
@@ -44,8 +44,12 @@ export declare const OUTREACH_RAMP_STEPS: ReadonlyArray<{
|
|
|
44
44
|
}>;
|
|
45
45
|
/** Mon–Fri, 9:00 to 17:00 — the window a new mailbox starts with. */
|
|
46
46
|
export declare const OUTREACH_DEFAULT_WINDOW: OutreachSendWindow;
|
|
47
|
-
/**
|
|
48
|
-
|
|
47
|
+
/**
|
|
48
|
+
* How many mailboxes one member may connect in one organization: room for a
|
|
49
|
+
* rep's own mailbox beside a set of cold-email inboxes a sequence sends from
|
|
50
|
+
* in rotation (AGL-3489).
|
|
51
|
+
*/
|
|
52
|
+
export declare const OUTREACH_MAX_MAILBOXES_PER_MEMBER = 10;
|
|
49
53
|
/** The longest From name accepted. */
|
|
50
54
|
export declare const OUTREACH_DISPLAY_NAME_MAX = 100;
|
|
51
55
|
/** Day labels, `0` Sunday, as the window's `days` count them. */
|
|
@@ -50,7 +50,11 @@ import { OUTREACH_DAILY_CAP_MAX, OUTREACH_RAMP_DAILY_CAPS, outreachDailyCap } fr
|
|
|
50
50
|
startMinute: 9 * 60,
|
|
51
51
|
endMinute: 17 * 60
|
|
52
52
|
};
|
|
53
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* How many mailboxes one member may connect in one organization: room for a
|
|
55
|
+
* rep's own mailbox beside a set of cold-email inboxes a sequence sends from
|
|
56
|
+
* in rotation (AGL-3489).
|
|
57
|
+
*/ export const OUTREACH_MAX_MAILBOXES_PER_MEMBER = 10;
|
|
54
58
|
/** The longest From name accepted. */ export const OUTREACH_DISPLAY_NAME_MAX = 100;
|
|
55
59
|
/** Day labels, `0` Sunday, as the window's `days` count them. */ export const OUTREACH_WEEKDAY_LABELS = [
|
|
56
60
|
'Sun',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/mailboxes/mailbox-settings.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 { isValidTimeZone, zonedDayKey } from '@aglyn/shared-util-timestamp/zoned-time'\nimport {\n OUTREACH_DAILY_CAP_MAX,\n OUTREACH_RAMP_DAILY_CAPS,\n outreachDailyCap,\n} from '../engine/sending-capacity'\nimport type {\n OutreachMailbox,\n OutreachMailboxHealth,\n OutreachMailboxStatus,\n OutreachSendAsAddress,\n OutreachSendWindow,\n} from '../model/outreach.types'\n\n/**\n * A MAILBOX'S SETTINGS, AS BOTH HALVES READ THEM (AGL-2978).\n *\n * The rules a mailbox's settings are held to — the cap, the ramp, the sending\n * window, the timezone, which send-as addresses count — in one client-safe\n * module, so the panel that greys out a field and the route that refuses the\n * write give the same answer.\n *\n * The ceiling, the ramp and a mailbox's day are the engine's\n * (`../engine/sending-capacity.ts`, AGL-2979) and the shared zoned-time\n * helpers': the panel shows today's limit with the very function the sending\n * runtime enforces it with, so the two cannot drift.\n */\n\n/** The daily cap a new mailbox starts with. */\nexport const OUTREACH_DEFAULT_DAILY_CAP = 20\n\n/** The most a mailbox may send in one day, whatever it is set to: the engine's ceiling. */\nexport const OUTREACH_MAX_DAILY_CAP = OUTREACH_DAILY_CAP_MAX\n\n/**\n * The warm-up ramp, as the panel prints it: the most a mailbox sends a day in\n * its first week, its second, and every week after — the engine's\n * `OUTREACH_RAMP_DAILY_CAPS`. The cap still applies on top, so a mailbox\n * capped at 20 sends 20 in week three, not 30.\n */\nexport const OUTREACH_RAMP_STEPS: ReadonlyArray<{ week: number; limit: number }> =\n OUTREACH_RAMP_DAILY_CAPS.map((limit, index) => ({ week: index + 1, limit }))\n\n/** Mon–Fri, 9:00 to 17:00 — the window a new mailbox starts with. */\nexport const OUTREACH_DEFAULT_WINDOW: OutreachSendWindow = {\n days: [1, 2, 3, 4, 5],\n startMinute: 9 * 60,\n endMinute: 17 * 60,\n}\n\n/** How many mailboxes one member may connect in one organization. */\nexport const OUTREACH_MAX_MAILBOXES_PER_MEMBER = 5\n\n/** The longest From name accepted. */\nexport const OUTREACH_DISPLAY_NAME_MAX = 100\n\n/** Day labels, `0` Sunday, as the window's `days` count them. */\nexport const OUTREACH_WEEKDAY_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] as const\n\n/**\n * Today's limit: the cap, lowered by the ramp while it runs — the engine's\n * `outreachDailyCap`, read in the mailbox's zone (UTC for one it does not\n * know), with the stored cap read defensively first.\n */\nexport function outreachEffectiveDailyCap(\n mailbox: Pick<OutreachMailbox, 'dailyCap' | 'rampStartedAtMs' | 'timezone'>,\n nowMs: number,\n): number {\n return outreachDailyCap({\n configuredCap: clampDailyCap(mailbox.dailyCap),\n rampStartedAtMs: mailbox.rampStartedAtMs,\n nowMs,\n timeZone: isValidTimezone(mailbox.timezone) ? mailbox.timezone : 'UTC',\n })\n}\n\n/** A stored cap read defensively: an integer from 1 to the maximum. */\nexport function clampDailyCap(value: unknown): number {\n const number = Math.floor(Number(value))\n if (!Number.isFinite(number) || number < 1) return OUTREACH_DEFAULT_DAILY_CAP\n return Math.min(number, OUTREACH_MAX_DAILY_CAP)\n}\n\n/** Why a settings value was refused, in the words the panel shows. */\nexport type OutreachSettingsRefusal = { field: string; message: string }\n\n/** A cap as typed: a whole number from 1 to the ceiling, or a refusal. */\nexport function validateDailyCap(value: unknown): number | OutreachSettingsRefusal {\n const number = typeof value === 'number' ? value : Number.NaN\n if (!Number.isInteger(number) || number < 1 || number > OUTREACH_MAX_DAILY_CAP) {\n return {\n field: 'dailyCap',\n message: `The daily cap must be a whole number from 1 to ${OUTREACH_MAX_DAILY_CAP}.`,\n }\n }\n return number\n}\n\n/** A window as sent: days 0–6 at least once, whole minutes, start before end. */\nexport function validateSendWindow(value: unknown): OutreachSendWindow | OutreachSettingsRefusal {\n const record = (value ?? {}) as Partial<OutreachSendWindow>\n const days = Array.isArray(record.days) ? record.days : []\n const unique = [...new Set(days)]\n if (!unique.length || !unique.every((day) => Number.isInteger(day) && day >= 0 && day <= 6)) {\n return { field: 'window', message: 'Choose at least one day to send on.' }\n }\n const start = Number(record.startMinute)\n const end = Number(record.endMinute)\n if (\n !Number.isInteger(start) ||\n !Number.isInteger(end) ||\n start < 0 ||\n end > 24 * 60 ||\n start >= end\n ) {\n return { field: 'window', message: 'The sending window must start before it ends, within one day.' }\n }\n return { days: unique.sort((a, b) => a - b), startMinute: start, endMinute: end }\n}\n\n/** Whether a string names an IANA timezone this runtime knows, of a sane length to store. */\nexport function isValidTimezone(value: unknown): value is string {\n return typeof value === 'string' && value.length <= 64 && isValidTimeZone(value)\n}\n\n/** Whether text holds a character below space, or DEL — a line break included. */\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 === 127) return true\n }\n return false\n}\n\n/** A From name as typed: one line, not too long, or a refusal. */\nexport function validateDisplayName(value: unknown): string | OutreachSettingsRefusal {\n const name = typeof value === 'string' ? value.trim() : ''\n if (hasControlCharacter(name) || name.length > OUTREACH_DISPLAY_NAME_MAX) {\n return {\n field: 'displayName',\n message: `The display name must be one line of at most ${OUTREACH_DISPLAY_NAME_MAX} characters.`,\n }\n }\n return name\n}\n\n/** `9:00`, `17:30`, `24:00` — a minute of the day as the panel prints it. */\nexport function formatMinuteOfDay(minute: number): string {\n const hours = Math.floor(minute / 60)\n return `${hours}:${String(minute % 60).padStart(2, '0')}`\n}\n\n/** A status as the panel names it. */\nexport const OUTREACH_MAILBOX_STATUS_LABELS: Record<OutreachMailboxStatus, string> = {\n connected: 'Active',\n paused: 'Paused',\n reconnect_required: 'Reconnect required',\n disconnected: 'Disconnected',\n}\n\n/**\n * `YYYY-MM-DD` for an instant, in a timezone (UTC for an unknown one) — the\n * day key the sending runtime counts a mailbox's sends under.\n */\nexport function outreachLocalDay(nowMs: number, timezone: string): string {\n return zonedDayKey(nowMs, isValidTimezone(timezone) ? timezone : 'UTC')\n}\n\n/** The seven mailbox-local days ending today, newest first. */\nexport function outreachLastSevenDays(nowMs: number, timezone: string): string[] {\n const days: string[] = []\n // Stepping by whole days in UTC and formatting in the zone can repeat or\n // skip a day across a DST change, so collect distinct days until seven.\n for (let offset = 0; days.length < 7 && offset < 9; offset += 1) {\n const day = outreachLocalDay(nowMs - offset * 24 * 60 * 60 * 1000, timezone)\n if (!days.includes(day)) days.push(day)\n }\n return days\n}\n\n/** The last seven days of sending, summed, and whether anything was ever sent. */\nexport interface OutreachHealthSummary {\n sent: number\n bounces: number\n replies: number\n /** False until the mailbox's first send: the panel says \"No sends yet\". */\n hasSent: boolean\n}\n\nexport function summarizeMailboxHealth(\n health: Partial<OutreachMailboxHealth> | null | undefined,\n timezone: string,\n nowMs: number,\n): OutreachHealthSummary {\n const daily = health?.daily ?? {}\n const summary = { sent: 0, bounces: 0, replies: 0 }\n for (const day of outreachLastSevenDays(nowMs, timezone)) {\n const counts = daily[day]\n if (!counts) continue\n summary.sent += Math.max(0, Number(counts.sent) || 0)\n summary.bounces += Math.max(0, Number(counts.bounces) || 0)\n summary.replies += Math.max(0, Number(counts.replies) || 0)\n }\n const everSent =\n typeof health?.lastSentAtMs === 'number' ||\n Object.values(daily).some((counts) => Number(counts?.sent) > 0)\n return { ...summary, hasSent: everSent }\n}\n\n/** Gmail's send-as entry, as the transport reads it. */\nexport interface SendAsListing {\n sendAsEmail: string\n displayName: string\n isPrimary: boolean\n isDefault: boolean\n verificationStatus: string | null\n}\n\n/**\n * The send-as addresses a mailbox may offer: the account's own address, and\n * every alias Gmail marks `accepted`. A `pending` alias is one Gmail is still\n * waiting for its owner to confirm, and sending as it would be sending as an\n * address nobody has shown is theirs.\n */\nexport function verifiedSendAsAddresses(listing: readonly SendAsListing[]): OutreachSendAsAddress[] {\n const seen = new Set<string>()\n const addresses: OutreachSendAsAddress[] = []\n for (const entry of listing) {\n const email = String(entry.sendAsEmail ?? '').trim().toLowerCase()\n if (!email || seen.has(email)) continue\n if (!entry.isPrimary && entry.verificationStatus !== 'accepted') continue\n seen.add(email)\n addresses.push({\n email,\n displayName: String(entry.displayName ?? '').trim(),\n isPrimary: Boolean(entry.isPrimary),\n isDefault: Boolean(entry.isDefault),\n })\n }\n return addresses\n}\n\n/** The send-as a new mailbox starts with: Gmail's default, else its own address. */\nexport function defaultSendAs(options: readonly OutreachSendAsAddress[], accountEmail: string): string {\n return (\n options.find((option) => option.isDefault)?.email ??\n options.find((option) => option.isPrimary)?.email ??\n accountEmail\n )\n}\n"],"names":["isValidTimeZone","zonedDayKey","OUTREACH_DAILY_CAP_MAX","OUTREACH_RAMP_DAILY_CAPS","outreachDailyCap","OUTREACH_DEFAULT_DAILY_CAP","OUTREACH_MAX_DAILY_CAP","OUTREACH_RAMP_STEPS","map","limit","index","week","OUTREACH_DEFAULT_WINDOW","days","startMinute","endMinute","OUTREACH_MAX_MAILBOXES_PER_MEMBER","OUTREACH_DISPLAY_NAME_MAX","OUTREACH_WEEKDAY_LABELS","outreachEffectiveDailyCap","mailbox","nowMs","configuredCap","clampDailyCap","dailyCap","rampStartedAtMs","timeZone","isValidTimezone","timezone","value","number","Math","floor","Number","isFinite","min","validateDailyCap","NaN","isInteger","field","message","validateSendWindow","record","Array","isArray","unique","Set","length","every","day","start","end","sort","a","b","hasControlCharacter","code","charCodeAt","validateDisplayName","name","trim","formatMinuteOfDay","minute","hours","String","padStart","OUTREACH_MAILBOX_STATUS_LABELS","connected","paused","reconnect_required","disconnected","outreachLocalDay","outreachLastSevenDays","offset","includes","push","summarizeMailboxHealth","health","daily","summary","sent","bounces","replies","counts","max","everSent","lastSentAtMs","Object","values","some","hasSent","verifiedSendAsAddresses","listing","seen","addresses","entry","email","sendAsEmail","toLowerCase","has","isPrimary","verificationStatus","add","displayName","Boolean","isDefault","defaultSendAs","options","accountEmail","find","option"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/mailboxes/mailbox-settings.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 { isValidTimeZone, zonedDayKey } from '@aglyn/shared-util-timestamp/zoned-time'\nimport {\n OUTREACH_DAILY_CAP_MAX,\n OUTREACH_RAMP_DAILY_CAPS,\n outreachDailyCap,\n} from '../engine/sending-capacity'\nimport type {\n OutreachMailbox,\n OutreachMailboxHealth,\n OutreachMailboxStatus,\n OutreachSendAsAddress,\n OutreachSendWindow,\n} from '../model/outreach.types'\n\n/**\n * A MAILBOX'S SETTINGS, AS BOTH HALVES READ THEM (AGL-2978).\n *\n * The rules a mailbox's settings are held to — the cap, the ramp, the sending\n * window, the timezone, which send-as addresses count — in one client-safe\n * module, so the panel that greys out a field and the route that refuses the\n * write give the same answer.\n *\n * The ceiling, the ramp and a mailbox's day are the engine's\n * (`../engine/sending-capacity.ts`, AGL-2979) and the shared zoned-time\n * helpers': the panel shows today's limit with the very function the sending\n * runtime enforces it with, so the two cannot drift.\n */\n\n/** The daily cap a new mailbox starts with. */\nexport const OUTREACH_DEFAULT_DAILY_CAP = 20\n\n/** The most a mailbox may send in one day, whatever it is set to: the engine's ceiling. */\nexport const OUTREACH_MAX_DAILY_CAP = OUTREACH_DAILY_CAP_MAX\n\n/**\n * The warm-up ramp, as the panel prints it: the most a mailbox sends a day in\n * its first week, its second, and every week after — the engine's\n * `OUTREACH_RAMP_DAILY_CAPS`. The cap still applies on top, so a mailbox\n * capped at 20 sends 20 in week three, not 30.\n */\nexport const OUTREACH_RAMP_STEPS: ReadonlyArray<{ week: number; limit: number }> =\n OUTREACH_RAMP_DAILY_CAPS.map((limit, index) => ({ week: index + 1, limit }))\n\n/** Mon–Fri, 9:00 to 17:00 — the window a new mailbox starts with. */\nexport const OUTREACH_DEFAULT_WINDOW: OutreachSendWindow = {\n days: [1, 2, 3, 4, 5],\n startMinute: 9 * 60,\n endMinute: 17 * 60,\n}\n\n/**\n * How many mailboxes one member may connect in one organization: room for a\n * rep's own mailbox beside a set of cold-email inboxes a sequence sends from\n * in rotation (AGL-3489).\n */\nexport const OUTREACH_MAX_MAILBOXES_PER_MEMBER = 10\n\n/** The longest From name accepted. */\nexport const OUTREACH_DISPLAY_NAME_MAX = 100\n\n/** Day labels, `0` Sunday, as the window's `days` count them. */\nexport const OUTREACH_WEEKDAY_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] as const\n\n/**\n * Today's limit: the cap, lowered by the ramp while it runs — the engine's\n * `outreachDailyCap`, read in the mailbox's zone (UTC for one it does not\n * know), with the stored cap read defensively first.\n */\nexport function outreachEffectiveDailyCap(\n mailbox: Pick<OutreachMailbox, 'dailyCap' | 'rampStartedAtMs' | 'timezone'>,\n nowMs: number,\n): number {\n return outreachDailyCap({\n configuredCap: clampDailyCap(mailbox.dailyCap),\n rampStartedAtMs: mailbox.rampStartedAtMs,\n nowMs,\n timeZone: isValidTimezone(mailbox.timezone) ? mailbox.timezone : 'UTC',\n })\n}\n\n/** A stored cap read defensively: an integer from 1 to the maximum. */\nexport function clampDailyCap(value: unknown): number {\n const number = Math.floor(Number(value))\n if (!Number.isFinite(number) || number < 1) return OUTREACH_DEFAULT_DAILY_CAP\n return Math.min(number, OUTREACH_MAX_DAILY_CAP)\n}\n\n/** Why a settings value was refused, in the words the panel shows. */\nexport type OutreachSettingsRefusal = { field: string; message: string }\n\n/** A cap as typed: a whole number from 1 to the ceiling, or a refusal. */\nexport function validateDailyCap(value: unknown): number | OutreachSettingsRefusal {\n const number = typeof value === 'number' ? value : Number.NaN\n if (!Number.isInteger(number) || number < 1 || number > OUTREACH_MAX_DAILY_CAP) {\n return {\n field: 'dailyCap',\n message: `The daily cap must be a whole number from 1 to ${OUTREACH_MAX_DAILY_CAP}.`,\n }\n }\n return number\n}\n\n/** A window as sent: days 0–6 at least once, whole minutes, start before end. */\nexport function validateSendWindow(value: unknown): OutreachSendWindow | OutreachSettingsRefusal {\n const record = (value ?? {}) as Partial<OutreachSendWindow>\n const days = Array.isArray(record.days) ? record.days : []\n const unique = [...new Set(days)]\n if (!unique.length || !unique.every((day) => Number.isInteger(day) && day >= 0 && day <= 6)) {\n return { field: 'window', message: 'Choose at least one day to send on.' }\n }\n const start = Number(record.startMinute)\n const end = Number(record.endMinute)\n if (\n !Number.isInteger(start) ||\n !Number.isInteger(end) ||\n start < 0 ||\n end > 24 * 60 ||\n start >= end\n ) {\n return { field: 'window', message: 'The sending window must start before it ends, within one day.' }\n }\n return { days: unique.sort((a, b) => a - b), startMinute: start, endMinute: end }\n}\n\n/** Whether a string names an IANA timezone this runtime knows, of a sane length to store. */\nexport function isValidTimezone(value: unknown): value is string {\n return typeof value === 'string' && value.length <= 64 && isValidTimeZone(value)\n}\n\n/** Whether text holds a character below space, or DEL — a line break included. */\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 === 127) return true\n }\n return false\n}\n\n/** A From name as typed: one line, not too long, or a refusal. */\nexport function validateDisplayName(value: unknown): string | OutreachSettingsRefusal {\n const name = typeof value === 'string' ? value.trim() : ''\n if (hasControlCharacter(name) || name.length > OUTREACH_DISPLAY_NAME_MAX) {\n return {\n field: 'displayName',\n message: `The display name must be one line of at most ${OUTREACH_DISPLAY_NAME_MAX} characters.`,\n }\n }\n return name\n}\n\n/** `9:00`, `17:30`, `24:00` — a minute of the day as the panel prints it. */\nexport function formatMinuteOfDay(minute: number): string {\n const hours = Math.floor(minute / 60)\n return `${hours}:${String(minute % 60).padStart(2, '0')}`\n}\n\n/** A status as the panel names it. */\nexport const OUTREACH_MAILBOX_STATUS_LABELS: Record<OutreachMailboxStatus, string> = {\n connected: 'Active',\n paused: 'Paused',\n reconnect_required: 'Reconnect required',\n disconnected: 'Disconnected',\n}\n\n/**\n * `YYYY-MM-DD` for an instant, in a timezone (UTC for an unknown one) — the\n * day key the sending runtime counts a mailbox's sends under.\n */\nexport function outreachLocalDay(nowMs: number, timezone: string): string {\n return zonedDayKey(nowMs, isValidTimezone(timezone) ? timezone : 'UTC')\n}\n\n/** The seven mailbox-local days ending today, newest first. */\nexport function outreachLastSevenDays(nowMs: number, timezone: string): string[] {\n const days: string[] = []\n // Stepping by whole days in UTC and formatting in the zone can repeat or\n // skip a day across a DST change, so collect distinct days until seven.\n for (let offset = 0; days.length < 7 && offset < 9; offset += 1) {\n const day = outreachLocalDay(nowMs - offset * 24 * 60 * 60 * 1000, timezone)\n if (!days.includes(day)) days.push(day)\n }\n return days\n}\n\n/** The last seven days of sending, summed, and whether anything was ever sent. */\nexport interface OutreachHealthSummary {\n sent: number\n bounces: number\n replies: number\n /** False until the mailbox's first send: the panel says \"No sends yet\". */\n hasSent: boolean\n}\n\nexport function summarizeMailboxHealth(\n health: Partial<OutreachMailboxHealth> | null | undefined,\n timezone: string,\n nowMs: number,\n): OutreachHealthSummary {\n const daily = health?.daily ?? {}\n const summary = { sent: 0, bounces: 0, replies: 0 }\n for (const day of outreachLastSevenDays(nowMs, timezone)) {\n const counts = daily[day]\n if (!counts) continue\n summary.sent += Math.max(0, Number(counts.sent) || 0)\n summary.bounces += Math.max(0, Number(counts.bounces) || 0)\n summary.replies += Math.max(0, Number(counts.replies) || 0)\n }\n const everSent =\n typeof health?.lastSentAtMs === 'number' ||\n Object.values(daily).some((counts) => Number(counts?.sent) > 0)\n return { ...summary, hasSent: everSent }\n}\n\n/** Gmail's send-as entry, as the transport reads it. */\nexport interface SendAsListing {\n sendAsEmail: string\n displayName: string\n isPrimary: boolean\n isDefault: boolean\n verificationStatus: string | null\n}\n\n/**\n * The send-as addresses a mailbox may offer: the account's own address, and\n * every alias Gmail marks `accepted`. A `pending` alias is one Gmail is still\n * waiting for its owner to confirm, and sending as it would be sending as an\n * address nobody has shown is theirs.\n */\nexport function verifiedSendAsAddresses(listing: readonly SendAsListing[]): OutreachSendAsAddress[] {\n const seen = new Set<string>()\n const addresses: OutreachSendAsAddress[] = []\n for (const entry of listing) {\n const email = String(entry.sendAsEmail ?? '').trim().toLowerCase()\n if (!email || seen.has(email)) continue\n if (!entry.isPrimary && entry.verificationStatus !== 'accepted') continue\n seen.add(email)\n addresses.push({\n email,\n displayName: String(entry.displayName ?? '').trim(),\n isPrimary: Boolean(entry.isPrimary),\n isDefault: Boolean(entry.isDefault),\n })\n }\n return addresses\n}\n\n/** The send-as a new mailbox starts with: Gmail's default, else its own address. */\nexport function defaultSendAs(options: readonly OutreachSendAsAddress[], accountEmail: string): string {\n return (\n options.find((option) => option.isDefault)?.email ??\n options.find((option) => option.isPrimary)?.email ??\n accountEmail\n )\n}\n"],"names":["isValidTimeZone","zonedDayKey","OUTREACH_DAILY_CAP_MAX","OUTREACH_RAMP_DAILY_CAPS","outreachDailyCap","OUTREACH_DEFAULT_DAILY_CAP","OUTREACH_MAX_DAILY_CAP","OUTREACH_RAMP_STEPS","map","limit","index","week","OUTREACH_DEFAULT_WINDOW","days","startMinute","endMinute","OUTREACH_MAX_MAILBOXES_PER_MEMBER","OUTREACH_DISPLAY_NAME_MAX","OUTREACH_WEEKDAY_LABELS","outreachEffectiveDailyCap","mailbox","nowMs","configuredCap","clampDailyCap","dailyCap","rampStartedAtMs","timeZone","isValidTimezone","timezone","value","number","Math","floor","Number","isFinite","min","validateDailyCap","NaN","isInteger","field","message","validateSendWindow","record","Array","isArray","unique","Set","length","every","day","start","end","sort","a","b","hasControlCharacter","code","charCodeAt","validateDisplayName","name","trim","formatMinuteOfDay","minute","hours","String","padStart","OUTREACH_MAILBOX_STATUS_LABELS","connected","paused","reconnect_required","disconnected","outreachLocalDay","outreachLastSevenDays","offset","includes","push","summarizeMailboxHealth","health","daily","summary","sent","bounces","replies","counts","max","everSent","lastSentAtMs","Object","values","some","hasSent","verifiedSendAsAddresses","listing","seen","addresses","entry","email","sendAsEmail","toLowerCase","has","isPrimary","verificationStatus","add","displayName","Boolean","isDefault","defaultSendAs","options","accountEmail","find","option"],"mappings":"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"}
|
|
@@ -14,17 +14,21 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
-
import { type GmailClient } from '../transport/gmail-client';
|
|
18
17
|
import type { TransportDeps } from '../transport/http';
|
|
19
|
-
import {
|
|
20
|
-
import { type
|
|
18
|
+
import type { OutreachMailClient } from '../transport/mail-client';
|
|
19
|
+
import { type OutreachStoredMailboxCredentials } from './mailbox-credentials';
|
|
20
|
+
import { type OutreachGoogleConfigResult, type OutreachMicrosoftConfigResult } from './outreach-config';
|
|
21
21
|
/**
|
|
22
22
|
* A CONNECTED MAILBOX, OPENED FOR SENDING (AGL-2978).
|
|
23
23
|
*
|
|
24
24
|
* The door the send and sync runtime, and the panel's test send, reach a
|
|
25
|
-
* mailbox
|
|
26
|
-
*
|
|
27
|
-
*
|
|
25
|
+
* mailbox through: the stored credential read, its refresh token opened with
|
|
26
|
+
* the deployment's key, and the transport client for its provider — Gmail,
|
|
27
|
+
* or Microsoft Graph (AGL-3489) — built on it. The token never leaves this
|
|
28
|
+
* function except inside the client.
|
|
29
|
+
*
|
|
30
|
+
* Microsoft rotates refresh tokens; the Graph client hands a rotated one
|
|
31
|
+
* back, and it is sealed and stored in place of the one it replaced.
|
|
28
32
|
*
|
|
29
33
|
* A token opened under an older key of the keyring is sealed again under the
|
|
30
34
|
* current one on the way through, so a key rotation completes itself as
|
|
@@ -32,12 +36,13 @@ import { type OutreachGoogleConfigResult } from './outreach-config';
|
|
|
32
36
|
*/
|
|
33
37
|
export type OpenedOutreachMailbox = {
|
|
34
38
|
ok: true;
|
|
35
|
-
client:
|
|
36
|
-
credential:
|
|
39
|
+
client: OutreachMailClient;
|
|
40
|
+
credential: OutreachStoredMailboxCredentials;
|
|
37
41
|
} | {
|
|
38
42
|
ok: false;
|
|
39
43
|
/**
|
|
40
|
-
* - `not-configured` — this deployment has no Outreach client or key
|
|
44
|
+
* - `not-configured` — this deployment has no Outreach client or key
|
|
45
|
+
* for the mailbox's provider.
|
|
41
46
|
* - `no-credential` — nothing is stored for the mailbox.
|
|
42
47
|
* - `sealed-token-unreadable` — the stored token does not open with
|
|
43
48
|
* this deployment's key (rotated away, or altered). The mailbox needs
|
|
@@ -47,6 +52,7 @@ export type OpenedOutreachMailbox = {
|
|
|
47
52
|
};
|
|
48
53
|
export interface OpenMailboxDeps extends TransportDeps {
|
|
49
54
|
readConfig?: () => OutreachGoogleConfigResult;
|
|
55
|
+
readMicrosoftConfig?: () => OutreachMicrosoftConfigResult;
|
|
50
56
|
}
|
|
51
57
|
export declare function openOutreachMailboxClient(firestore: FirebaseFirestore.Firestore, input: {
|
|
52
58
|
mailboxId: string;
|
|
@@ -16,23 +16,27 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
16
16
|
* limitations under the License.
|
|
17
17
|
*/ import { SecretBoxError } from "@aglyn/shared-util-tools/secret-box";
|
|
18
18
|
import { createGmailClient } from "../transport/gmail-client.js";
|
|
19
|
+
import { createGraphClient } from "../transport/graph-client.js";
|
|
19
20
|
import { mailboxCredentialsRef, mailboxRef, openMailboxRefreshToken, readMailboxCredentials, sealMailboxRefreshToken } from "./mailbox-credentials.js";
|
|
20
|
-
import { readOutreachGoogleConfig } from "./outreach-config.js";
|
|
21
|
+
import { readOutreachGoogleConfig, readOutreachMicrosoftConfig } from "./outreach-config.js";
|
|
21
22
|
export async function openOutreachMailboxClient(firestore, input, deps = {}) {
|
|
22
|
-
var _deps_readConfig;
|
|
23
|
-
const config = ((_deps_readConfig = deps.readConfig) != null ? _deps_readConfig : readOutreachGoogleConfig)();
|
|
24
|
-
if (!config.configured) return {
|
|
25
|
-
ok: false,
|
|
26
|
-
reason: 'not-configured'
|
|
27
|
-
};
|
|
23
|
+
var _deps_readMicrosoftConfig, _deps_readConfig;
|
|
28
24
|
const snapshot = await mailboxCredentialsRef(firestore, input.mailboxId).get();
|
|
29
25
|
const credential = readMailboxCredentials(snapshot.exists ? snapshot.data() : null);
|
|
30
26
|
if (!credential || credential.mailboxId !== input.mailboxId) {
|
|
27
|
+
var _deps_readConfig1, _deps_readMicrosoftConfig1;
|
|
28
|
+
const anyConfigured = ((_deps_readConfig1 = deps.readConfig) != null ? _deps_readConfig1 : readOutreachGoogleConfig)().configured || ((_deps_readMicrosoftConfig1 = deps.readMicrosoftConfig) != null ? _deps_readMicrosoftConfig1 : readOutreachMicrosoftConfig)().configured;
|
|
31
29
|
return {
|
|
32
30
|
ok: false,
|
|
33
|
-
reason: 'no-credential'
|
|
31
|
+
reason: anyConfigured ? 'no-credential' : 'not-configured'
|
|
34
32
|
};
|
|
35
33
|
}
|
|
34
|
+
const microsoft = credential.provider === 'microsoft' ? ((_deps_readMicrosoftConfig = deps.readMicrosoftConfig) != null ? _deps_readMicrosoftConfig : readOutreachMicrosoftConfig)() : null;
|
|
35
|
+
const config = microsoft != null ? microsoft : ((_deps_readConfig = deps.readConfig) != null ? _deps_readConfig : readOutreachGoogleConfig)();
|
|
36
|
+
if (!config.configured) return {
|
|
37
|
+
ok: false,
|
|
38
|
+
reason: 'not-configured'
|
|
39
|
+
};
|
|
36
40
|
let opened;
|
|
37
41
|
try {
|
|
38
42
|
opened = openMailboxRefreshToken(credential, config.config.keyring);
|
|
@@ -43,8 +47,30 @@ export async function openOutreachMailboxClient(firestore, input, deps = {}) {
|
|
|
43
47
|
};
|
|
44
48
|
throw error;
|
|
45
49
|
}
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
const resealed = opened.needsReseal ? await resealMailboxRefreshToken(firestore, credential, opened.refreshToken, config.config.keyring, deps.now) : null;
|
|
51
|
+
if (microsoft == null ? void 0 : microsoft.configured) {
|
|
52
|
+
const keyring = microsoft.config.keyring;
|
|
53
|
+
// The seal the stored document holds now: a rotation replaces it, and
|
|
54
|
+
// the next rotation is guarded against that one.
|
|
55
|
+
let stored = resealed ? _extends({}, credential, resealed) : credential;
|
|
56
|
+
const client = createGraphClient(_extends({}, deps, {
|
|
57
|
+
clientId: microsoft.config.clientId,
|
|
58
|
+
clientSecret: microsoft.config.clientSecret,
|
|
59
|
+
tenant: microsoft.config.tenant,
|
|
60
|
+
refreshToken: opened.refreshToken,
|
|
61
|
+
selfAddresses: [
|
|
62
|
+
credential.email
|
|
63
|
+
],
|
|
64
|
+
onRefreshTokenRotated: async (refreshToken)=>{
|
|
65
|
+
const written = await resealMailboxRefreshToken(firestore, stored, refreshToken, keyring, deps.now);
|
|
66
|
+
if (written) stored = _extends({}, stored, written);
|
|
67
|
+
}
|
|
68
|
+
}));
|
|
69
|
+
return {
|
|
70
|
+
ok: true,
|
|
71
|
+
client,
|
|
72
|
+
credential
|
|
73
|
+
};
|
|
48
74
|
}
|
|
49
75
|
const client = createGmailClient(_extends({}, deps, {
|
|
50
76
|
clientId: config.config.clientId,
|
|
@@ -58,22 +84,26 @@ export async function openOutreachMailboxClient(firestore, input, deps = {}) {
|
|
|
58
84
|
};
|
|
59
85
|
}
|
|
60
86
|
/**
|
|
61
|
-
* Seals a token
|
|
62
|
-
* still the one that was opened: a
|
|
63
|
-
* newer grant, and that one wins.
|
|
64
|
-
* seal, which still opens.
|
|
87
|
+
* Seals a token — the same one under the current key, or one the provider
|
|
88
|
+
* rotated in — only if the stored value is still the one that was opened: a
|
|
89
|
+
* reconnect that landed in between wrote a newer grant, and that one wins.
|
|
90
|
+
* Best-effort — a failure leaves the old seal, which still opens. Answers
|
|
91
|
+
* what it wrote, or `null` when it wrote nothing.
|
|
65
92
|
*/ async function resealMailboxRefreshToken(firestore, credential, refreshToken, keyring, now) {
|
|
66
93
|
const ref = mailboxCredentialsRef(firestore, credential.mailboxId);
|
|
67
94
|
try {
|
|
68
|
-
await firestore.runTransaction(async (tx)=>{
|
|
95
|
+
return await firestore.runTransaction(async (tx)=>{
|
|
69
96
|
const current = await tx.get(ref);
|
|
70
|
-
if (current.get('sealedRefreshToken') !== credential.sealedRefreshToken) return;
|
|
71
|
-
|
|
97
|
+
if (current.get('sealedRefreshToken') !== credential.sealedRefreshToken) return null;
|
|
98
|
+
const sealed = sealMailboxRefreshToken(refreshToken, credential.mailboxId, keyring);
|
|
99
|
+
tx.update(ref, _extends({}, sealed, {
|
|
72
100
|
updatedAtMs: (now != null ? now : Date.now)()
|
|
73
101
|
}));
|
|
102
|
+
return sealed;
|
|
74
103
|
});
|
|
75
104
|
} catch (error) {
|
|
76
105
|
console.error('[outreach] resealing a mailbox token failed', error);
|
|
106
|
+
return null;
|
|
77
107
|
}
|
|
78
108
|
}
|
|
79
109
|
/**
|