@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-revoke.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 { SecretBoxError } from '@aglyn/shared-util-tools/secret-box'\nimport { GmailTransportError } from '../transport/gmail-errors'\nimport { revokeGoogleToken } from '../transport/google-oauth'\nimport type { TransportDeps } from '../transport/http'\nimport type { OutreachMailboxDisconnectResponse } from './mailbox-api'\nimport {\n countOtherCredentialsForAccount,\n openMailboxRefreshToken,\n type
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/mailboxes/mailbox-revoke.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 { SecretBoxError } from '@aglyn/shared-util-tools/secret-box'\nimport { GmailTransportError } from '../transport/gmail-errors'\nimport { revokeGoogleToken } from '../transport/google-oauth'\nimport type { TransportDeps } from '../transport/http'\nimport type { OutreachMailboxDisconnectResponse } from './mailbox-api'\nimport {\n countOtherCredentialsForAccount,\n openMailboxRefreshToken,\n type OutreachStoredMailboxCredentials,\n} from './mailbox-credentials'\nimport type { OutreachGoogleConfigResult } from './outreach-config'\n\n/**\n * TELLING GOOGLE A STORED GRANT IS OVER (AGL-2978).\n *\n * A disconnect, a workspace erasure and an account erasure all end a\n * mailbox's grant the same way: open its sealed refresh token with this\n * deployment's key and revoke it at Google — unless another stored\n * credential still uses the same Google account, because Google's\n * revocation ends the whole grant this client holds for the account, and\n * revoking it would silently break that other mailbox.\n *\n * A Microsoft grant (AGL-3489) has no such endpoint: Microsoft offers no\n * way for an app to revoke one refresh token, so the outcome is\n * `unsupported`, and deleting the stored grant is what ends it here.\n *\n * Never throws. A grant that cannot be revoked is still deleted by whoever\n * called this; the outcome says which happened.\n */\n\n/**\n * What became of a grant at the provider: `revoked`, `already-invalid`,\n * `kept-for-other-mailbox` while another credential still uses the account,\n * `unsupported` for a provider with no revocation, or `failed` when Google\n * could not be told — the token would not open, the deployment is not\n * configured, or Google did not answer.\n */\nexport type OutreachGrantRevocation = OutreachMailboxDisconnectResponse['revocation']\n\nexport interface OutreachRevokeDeps {\n readConfig(): OutreachGoogleConfigResult\n /** The network and retry policy the revocation call uses. */\n transport: TransportDeps\n}\n\nexport interface RevokeMailboxGrantOptions {\n /**\n * Credentials of this organization do not count as another use of the\n * account: the workspace erasure is ending all of them.\n */\n excludeOrgId?: string\n /**\n * These credentials do not count as another use of the account either:\n * the account erasure is ending every one of the person's grants.\n */\n excludeMailboxIds?: ReadonlySet<string>\n}\n\n/** Revokes one stored grant at Google, unless it is still shared. Never throws. */\nexport async function revokeMailboxGrant(\n firestore: FirebaseFirestore.Firestore,\n credential: OutreachStoredMailboxCredentials,\n deps: OutreachRevokeDeps,\n options: RevokeMailboxGrantOptions = {},\n): Promise<OutreachGrantRevocation> {\n if (credential.provider === 'microsoft') return 'unsupported'\n try {\n const others = await countOtherCredentialsForAccount(firestore, {\n providerAccountId: credential.providerAccountId,\n mailboxId: credential.mailboxId,\n excludeOrgId: options.excludeOrgId,\n excludeMailboxIds: options.excludeMailboxIds,\n })\n if (others > 0) return 'kept-for-other-mailbox'\n const config = deps.readConfig()\n if (!config.configured) return 'failed'\n const { refreshToken } = openMailboxRefreshToken(credential, config.config.keyring)\n return await revokeGoogleToken(refreshToken, deps.transport)\n } catch (error) {\n if (!(error instanceof GmailTransportError) && !(error instanceof SecretBoxError)) {\n console.error('[outreach] revoking a mailbox grant failed', error)\n }\n return 'failed'\n }\n}\n"],"names":["SecretBoxError","GmailTransportError","revokeGoogleToken","countOtherCredentialsForAccount","openMailboxRefreshToken","revokeMailboxGrant","firestore","credential","deps","options","provider","others","providerAccountId","mailboxId","excludeOrgId","excludeMailboxIds","config","readConfig","configured","refreshToken","keyring","transport","error","console"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,cAAc,QAAQ,sCAAqC;AACpE,SAASC,mBAAmB,QAAQ,+BAA2B;AAC/D,SAASC,iBAAiB,QAAQ,+BAA2B;AAG7D,SACEC,+BAA+B,EAC/BC,uBAAuB,QAElB,2BAAuB;AAiD9B,iFAAiF,GACjF,OAAO,eAAeC,mBACpBC,SAAsC,EACtCC,UAA4C,EAC5CC,IAAwB,EACxBC,UAAqC,CAAC,CAAC;IAEvC,IAAIF,WAAWG,QAAQ,KAAK,aAAa,OAAO;IAChD,IAAI;QACF,MAAMC,SAAS,MAAMR,gCAAgCG,WAAW;YAC9DM,mBAAmBL,WAAWK,iBAAiB;YAC/CC,WAAWN,WAAWM,SAAS;YAC/BC,cAAcL,QAAQK,YAAY;YAClCC,mBAAmBN,QAAQM,iBAAiB;QAC9C;QACA,IAAIJ,SAAS,GAAG,OAAO;QACvB,MAAMK,SAASR,KAAKS,UAAU;QAC9B,IAAI,CAACD,OAAOE,UAAU,EAAE,OAAO;QAC/B,MAAM,EAAEC,YAAY,EAAE,GAAGf,wBAAwBG,YAAYS,OAAOA,MAAM,CAACI,OAAO;QAClF,OAAO,MAAMlB,kBAAkBiB,cAAcX,KAAKa,SAAS;IAC7D,EAAE,OAAOC,OAAO;QACd,IAAI,CAAEA,CAAAA,iBAAiBrB,mBAAkB,KAAM,CAAEqB,CAAAA,iBAAiBtB,cAAa,GAAI;YACjFuB,QAAQD,KAAK,CAAC,8CAA8CA;QAC9D;QACA,OAAO;IACT;AACF"}
|
|
@@ -18,10 +18,16 @@ import type { PluginWebApiHandler } from '@aglyn/aglyn/server';
|
|
|
18
18
|
import { type SenderReadiness, type SenderReadinessExpectation } from '@aglyn/shared-util-email';
|
|
19
19
|
import type { TransportDeps } from '../transport/http';
|
|
20
20
|
import { type OutreachGateDeps } from './mailbox-gate';
|
|
21
|
-
import { type OutreachGoogleConfigResult } from './outreach-config';
|
|
21
|
+
import { type OutreachGoogleConfigResult, type OutreachMicrosoftConfigResult } from './outreach-config';
|
|
22
22
|
/**
|
|
23
|
-
* THE MAILBOX ROUTES (AGL-2978): connect a rep's Google
|
|
24
|
-
* settings, pause it, test it, and disconnect it.
|
|
23
|
+
* THE MAILBOX ROUTES (AGL-2978): connect a rep's Google or Microsoft 365
|
|
24
|
+
* mailbox, keep its settings, pause it, test it, and disconnect it.
|
|
25
|
+
*
|
|
26
|
+
* A connect names its provider (AGL-3489). Both providers' consent screens
|
|
27
|
+
* return to the one callback; the signed state says which one a connect went
|
|
28
|
+
* to, and `connect/complete` redeems the code with that provider. Everything
|
|
29
|
+
* after the provider has named the account — the mailbox limit, the mailbox
|
|
30
|
+
* and its sealed grant, the activity row — is the same for both.
|
|
25
31
|
*
|
|
26
32
|
* ## The connect, end to end
|
|
27
33
|
*
|
|
@@ -73,6 +79,8 @@ export interface OutreachMailboxRouteDeps {
|
|
|
73
79
|
firestore(): FirebaseFirestore.Firestore;
|
|
74
80
|
gate: OutreachGateDeps;
|
|
75
81
|
readConfig(): OutreachGoogleConfigResult;
|
|
82
|
+
/** The Microsoft app registration (AGL-3489); the environment's when omitted. */
|
|
83
|
+
readMicrosoftConfig?(): OutreachMicrosoftConfigResult;
|
|
76
84
|
/** Whether the signing secret the OAuth state needs is configured. */
|
|
77
85
|
stateSigningConfigured(): boolean;
|
|
78
86
|
redirectUri(requestUrl: string): string | null;
|
|
@@ -15,10 +15,13 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
17
|
*/ import { normalizeContactEmail } from "@aglyn/aglyn/app-utils/contacts";
|
|
18
|
-
import { googleMailboxSenderExpectation } from "@aglyn/shared-util-email";
|
|
18
|
+
import { googleMailboxSenderExpectation, microsoftMailboxSenderExpectation } from "@aglyn/shared-util-email";
|
|
19
|
+
import { OUTREACH_MAILBOX_PROVIDERS } from "../model/outreach.types.js";
|
|
19
20
|
import { createGmailClient } from "../transport/gmail-client.js";
|
|
21
|
+
import { createGraphClient } from "../transport/graph-client.js";
|
|
20
22
|
import { GmailTransportError, googleFailureAnswer } from "../transport/gmail-errors.js";
|
|
21
23
|
import { buildGoogleAuthorizationUrl, exchangeGoogleAuthorizationCode, grantedScopesInclude, OUTREACH_REQUIRED_GRANTED_SCOPES, pkceChallenge, readGoogleIdToken } from "../transport/google-oauth.js";
|
|
24
|
+
import { buildMicrosoftAuthorizationUrl, exchangeMicrosoftAuthorizationCode, microsoftScopesInclude, readMicrosoftIdToken } from "../transport/microsoft-oauth.js";
|
|
22
25
|
import { Rfc5322MessageError } from "../transport/rfc5322.js";
|
|
23
26
|
import { sendOutreachMessage } from "../transport/send-message.js";
|
|
24
27
|
import { buildConnectReturnFragment } from "./mailbox-api.js";
|
|
@@ -28,7 +31,7 @@ import { outreachMemberGate, refusal } from "./mailbox-gate.js";
|
|
|
28
31
|
import { defaultSendAs, isValidTimezone, OUTREACH_DEFAULT_DAILY_CAP, OUTREACH_DEFAULT_WINDOW, OUTREACH_MAX_MAILBOXES_PER_MEMBER, validateDailyCap, validateDisplayName, validateSendWindow, verifiedSendAsAddresses } from "./mailbox-settings.js";
|
|
29
32
|
import { markOutreachMailboxReconnectRequired, openOutreachMailboxClient } from "./mailbox-transport.js";
|
|
30
33
|
import { consumeOutreachOAuthState, mintOutreachOAuthState, outreachOidcNonce, outreachPkceVerifier, readOutreachOAuthState, recordOutreachOAuthState } from "./oauth-state.js";
|
|
31
|
-
import {
|
|
34
|
+
import { outreachNotConfiguredMessage, readOutreachMicrosoftConfig } from "./outreach-config.js";
|
|
32
35
|
/** Test sends one mailbox may make in an hour. */ export const OUTREACH_TEST_SENDS_PER_HOUR = 5;
|
|
33
36
|
/**
|
|
34
37
|
* The org activity target a mailbox's rows are filed under: Outreach's own
|
|
@@ -58,9 +61,14 @@ async function readBody(request) {
|
|
|
58
61
|
const body = await request.json().catch(()=>null);
|
|
59
62
|
return body && typeof body === 'object' && !Array.isArray(body) ? body : {};
|
|
60
63
|
}
|
|
61
|
-
function notConfigured() {
|
|
62
|
-
return refusal(503, 'not-configured',
|
|
64
|
+
function notConfigured(provider = 'google') {
|
|
65
|
+
return refusal(503, 'not-configured', outreachNotConfiguredMessage(provider));
|
|
63
66
|
}
|
|
67
|
+
/** The provider a request names; Google when it names none, `null` for one that is not a provider. */ function readProvider(value) {
|
|
68
|
+
if (value === undefined || value === null || value === '') return 'google';
|
|
69
|
+
return OUTREACH_MAILBOX_PROVIDERS.includes(value) ? value : null;
|
|
70
|
+
}
|
|
71
|
+
const providerName = (provider)=>provider === 'microsoft' ? 'Microsoft' : 'Google';
|
|
64
72
|
/** A mailbox document read defensively into the model shape. */ function readMailbox(id, data) {
|
|
65
73
|
if (!data) return null;
|
|
66
74
|
return _extends({}, data, {
|
|
@@ -69,6 +77,11 @@ function notConfigured() {
|
|
|
69
77
|
}
|
|
70
78
|
const activityName = (mailbox)=>mailbox.email;
|
|
71
79
|
export function createOutreachMailboxRoutes(deps) {
|
|
80
|
+
const readMicrosoftConfig = ()=>{
|
|
81
|
+
var _deps_readMicrosoftConfig;
|
|
82
|
+
return ((_deps_readMicrosoftConfig = deps.readMicrosoftConfig) != null ? _deps_readMicrosoftConfig : readOutreachMicrosoftConfig)();
|
|
83
|
+
};
|
|
84
|
+
/** Whether a provider's client and the token key are configured here. */ const providerConfigured = (provider)=>provider === 'microsoft' ? readMicrosoftConfig().configured : deps.readConfig().configured;
|
|
72
85
|
/** The gate, then the mailbox the body names, then who may touch it. */ async function loadManagedMailbox(request, body, who) {
|
|
73
86
|
const gate = await outreachMemberGate(request, body['orgId'], deps.gate);
|
|
74
87
|
if (gate instanceof Response) return gate;
|
|
@@ -94,18 +107,30 @@ export function createOutreachMailboxRoutes(deps) {
|
|
|
94
107
|
// should see the whole list once rather than one variable per redeploy.
|
|
95
108
|
const missing = [];
|
|
96
109
|
const config = deps.readConfig();
|
|
110
|
+
const microsoft = readMicrosoftConfig();
|
|
97
111
|
if (config.configured === false) missing.push({
|
|
98
112
|
gate: 'google',
|
|
99
113
|
missing: config.missing
|
|
100
114
|
});
|
|
115
|
+
if (microsoft.configured === false) missing.push({
|
|
116
|
+
gate: 'microsoft',
|
|
117
|
+
missing: microsoft.missing
|
|
118
|
+
});
|
|
119
|
+
const shared = deps.stateSigningConfigured() && Boolean(deps.redirectUri(request.url));
|
|
101
120
|
if (!deps.stateSigningConfigured()) missing.push({
|
|
102
121
|
gate: 'state'
|
|
103
122
|
});
|
|
104
123
|
if (!deps.redirectUri(request.url)) missing.push({
|
|
105
124
|
gate: 'redirect'
|
|
106
125
|
});
|
|
126
|
+
const providers = {
|
|
127
|
+
google: shared && config.configured,
|
|
128
|
+
microsoft: shared && microsoft.configured
|
|
129
|
+
};
|
|
107
130
|
return ok(_extends({
|
|
108
|
-
|
|
131
|
+
// Google's readiness, as this field has always meant; `providers` says each.
|
|
132
|
+
configured: providers.google,
|
|
133
|
+
providers,
|
|
109
134
|
canManageAll: gate.isOrgAdmin
|
|
110
135
|
}, missing.length ? {
|
|
111
136
|
missing
|
|
@@ -116,9 +141,40 @@ export function createOutreachMailboxRoutes(deps) {
|
|
|
116
141
|
const body = await readBody(request);
|
|
117
142
|
const gate = await outreachMemberGate(request, body['orgId'], deps.gate);
|
|
118
143
|
if (gate instanceof Response) return gate;
|
|
119
|
-
const
|
|
144
|
+
const provider = readProvider(body['provider']);
|
|
145
|
+
if (!provider) return refusal(400, 'invalid-request', 'Choose Google or Microsoft.');
|
|
120
146
|
const redirectUri = deps.redirectUri(request.url);
|
|
121
|
-
if (!
|
|
147
|
+
if (!redirectUri || !deps.stateSigningConfigured()) return notConfigured(provider);
|
|
148
|
+
if (provider === 'microsoft') {
|
|
149
|
+
const microsoft = readMicrosoftConfig();
|
|
150
|
+
if (!microsoft.configured) return notConfigured(provider);
|
|
151
|
+
const nowMs = deps.now();
|
|
152
|
+
const { state, claims } = mintOutreachOAuthState({
|
|
153
|
+
orgId: gate.orgId,
|
|
154
|
+
uid: gate.uid,
|
|
155
|
+
nowMs,
|
|
156
|
+
provider
|
|
157
|
+
});
|
|
158
|
+
await recordOutreachOAuthState(deps.firestore(), {
|
|
159
|
+
claims,
|
|
160
|
+
redirectUri,
|
|
161
|
+
nowMs
|
|
162
|
+
});
|
|
163
|
+
const url = buildMicrosoftAuthorizationUrl({
|
|
164
|
+
clientId: microsoft.config.clientId,
|
|
165
|
+
tenant: microsoft.config.tenant,
|
|
166
|
+
redirectUri,
|
|
167
|
+
state,
|
|
168
|
+
codeChallenge: pkceChallenge(outreachPkceVerifier(claims.nonce)),
|
|
169
|
+
nonce: outreachOidcNonce(claims.nonce),
|
|
170
|
+
loginHint: typeof body['loginHint'] === 'string' ? normalizeContactEmail(body['loginHint']) : null
|
|
171
|
+
});
|
|
172
|
+
return ok({
|
|
173
|
+
url
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
const config = deps.readConfig();
|
|
177
|
+
if (!config.configured) return notConfigured();
|
|
122
178
|
const nowMs = deps.now();
|
|
123
179
|
const { state, claims } = mintOutreachOAuthState({
|
|
124
180
|
orgId: gate.orgId,
|
|
@@ -162,23 +218,27 @@ export function createOutreachMailboxRoutes(deps) {
|
|
|
162
218
|
const slug = typeof (org == null ? void 0 : org['slug']) === 'string' ? org['slug'] : '';
|
|
163
219
|
if (!slug) return plain(404, 'That organization could not be found.');
|
|
164
220
|
let fragment;
|
|
165
|
-
const
|
|
221
|
+
const providerError = params.get('error');
|
|
166
222
|
const code = (_params_get = params.get('code')) != null ? _params_get : '';
|
|
167
|
-
|
|
168
|
-
|
|
223
|
+
// An error fragment names a Microsoft connect, so the page says whose screen it was.
|
|
224
|
+
const named = claims.provider === 'microsoft' ? {
|
|
225
|
+
provider: claims.provider
|
|
226
|
+
} : {};
|
|
227
|
+
if (providerError) {
|
|
228
|
+
fragment = _extends({
|
|
169
229
|
kind: 'error',
|
|
170
|
-
reason:
|
|
171
|
-
};
|
|
230
|
+
reason: providerError === 'access_denied' ? 'access_denied' : 'google_error'
|
|
231
|
+
}, named);
|
|
172
232
|
} else if (!read.ok) {
|
|
173
|
-
fragment = {
|
|
233
|
+
fragment = _extends({
|
|
174
234
|
kind: 'error',
|
|
175
235
|
reason: 'expired'
|
|
176
|
-
};
|
|
236
|
+
}, named);
|
|
177
237
|
} else if (!code || code.length > MAX_CODE_CHARS) {
|
|
178
|
-
fragment = {
|
|
238
|
+
fragment = _extends({
|
|
179
239
|
kind: 'error',
|
|
180
240
|
reason: 'google_error'
|
|
181
|
-
};
|
|
241
|
+
}, named);
|
|
182
242
|
} else {
|
|
183
243
|
var _params_get1;
|
|
184
244
|
fragment = {
|
|
@@ -198,13 +258,12 @@ export function createOutreachMailboxRoutes(deps) {
|
|
|
198
258
|
});
|
|
199
259
|
};
|
|
200
260
|
const connectComplete = async (request)=>{
|
|
201
|
-
var _aliases_confirmed;
|
|
202
261
|
if (request.method !== 'POST') return methodNotAllowed('POST');
|
|
203
262
|
const body = await readBody(request);
|
|
204
263
|
const gate = await outreachMemberGate(request, body['orgId'], deps.gate);
|
|
205
264
|
if (gate instanceof Response) return gate;
|
|
206
|
-
|
|
207
|
-
if (!
|
|
265
|
+
if (!deps.stateSigningConfigured()) return notConfigured();
|
|
266
|
+
if (!deps.readConfig().configured && !readMicrosoftConfig().configured) return notConfigured();
|
|
208
267
|
const code = typeof body['code'] === 'string' ? body['code'] : '';
|
|
209
268
|
if (!code || code.length > MAX_CODE_CHARS) return refusal(400, 'invalid-request', 'The connection did not return a code.');
|
|
210
269
|
const firestore = deps.firestore();
|
|
@@ -221,6 +280,7 @@ export function createOutreachMailboxRoutes(deps) {
|
|
|
221
280
|
return refusal(400, 'state-org-mismatch', 'This connection was started in another organization.');
|
|
222
281
|
}
|
|
223
282
|
if (!read.ok) return refusal(410, 'state-expired', 'This connection took too long. Connect the mailbox again.');
|
|
283
|
+
if (!providerConfigured(claims.provider)) return notConfigured(claims.provider);
|
|
224
284
|
const consumed = await consumeOutreachOAuthState(firestore, {
|
|
225
285
|
claims,
|
|
226
286
|
nowMs
|
|
@@ -231,13 +291,36 @@ export function createOutreachMailboxRoutes(deps) {
|
|
|
231
291
|
}
|
|
232
292
|
return refusal(409, consumed.refusal, consumed.refusal === 'state-superseded' ? 'A newer connection was started. Finish that one, or connect the mailbox again.' : 'This connection was already used. Connect the mailbox again.');
|
|
233
293
|
}
|
|
294
|
+
const account = claims.provider === 'microsoft' ? await redeemMicrosoftCode({
|
|
295
|
+
code,
|
|
296
|
+
claims,
|
|
297
|
+
redirectUri: consumed.redirectUri,
|
|
298
|
+
nowMs
|
|
299
|
+
}) : await redeemGoogleCode({
|
|
300
|
+
code,
|
|
301
|
+
claims,
|
|
302
|
+
redirectUri: consumed.redirectUri,
|
|
303
|
+
nowMs
|
|
304
|
+
});
|
|
305
|
+
if (account instanceof Response) return account;
|
|
306
|
+
return storeConnectedMailbox({
|
|
307
|
+
gate,
|
|
308
|
+
body,
|
|
309
|
+
account,
|
|
310
|
+
nowMs
|
|
311
|
+
});
|
|
312
|
+
};
|
|
313
|
+
/** Google's half of a connect: the code redeemed, the grant and the account checked. */ async function redeemGoogleCode(input) {
|
|
314
|
+
const { code, claims, nowMs } = input;
|
|
315
|
+
const config = deps.readConfig();
|
|
316
|
+
if (!config.configured) return notConfigured();
|
|
234
317
|
let grant;
|
|
235
318
|
try {
|
|
236
319
|
grant = await exchangeGoogleAuthorizationCode({
|
|
237
320
|
clientId: config.config.clientId,
|
|
238
321
|
clientSecret: config.config.clientSecret,
|
|
239
322
|
code,
|
|
240
|
-
redirectUri:
|
|
323
|
+
redirectUri: input.redirectUri,
|
|
241
324
|
codeVerifier: outreachPkceVerifier(claims.nonce)
|
|
242
325
|
}, deps.transport);
|
|
243
326
|
} catch (error) {
|
|
@@ -300,7 +383,105 @@ export function createOutreachMailboxRoutes(deps) {
|
|
|
300
383
|
...sendAsOptions
|
|
301
384
|
];
|
|
302
385
|
}
|
|
303
|
-
|
|
386
|
+
return {
|
|
387
|
+
provider: 'google',
|
|
388
|
+
providerAccountId: identity.identity.sub,
|
|
389
|
+
email: profileEmail,
|
|
390
|
+
sendAsOptions,
|
|
391
|
+
scopes: grant.scope.split(/\s+/).filter(Boolean),
|
|
392
|
+
refreshToken: grant.refreshToken,
|
|
393
|
+
keyring: config.config.keyring
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Microsoft's half of a connect (AGL-3489): the code redeemed, the grant's
|
|
398
|
+
* permissions checked, the ID token read, and the account read back from
|
|
399
|
+
* Graph — the same account the sign-in named, by its directory id.
|
|
400
|
+
*/ async function redeemMicrosoftCode(input) {
|
|
401
|
+
const { code, claims, nowMs } = input;
|
|
402
|
+
const config = readMicrosoftConfig();
|
|
403
|
+
if (!config.configured) return notConfigured('microsoft');
|
|
404
|
+
const scopesMissing = ()=>refusal(422, 'scopes-missing', 'Sequences needs permission to send and to read your mail. Connect again and allow both.');
|
|
405
|
+
let grant;
|
|
406
|
+
try {
|
|
407
|
+
grant = await exchangeMicrosoftAuthorizationCode({
|
|
408
|
+
clientId: config.config.clientId,
|
|
409
|
+
clientSecret: config.config.clientSecret,
|
|
410
|
+
tenant: config.config.tenant,
|
|
411
|
+
code,
|
|
412
|
+
redirectUri: input.redirectUri,
|
|
413
|
+
codeVerifier: outreachPkceVerifier(claims.nonce)
|
|
414
|
+
}, deps.transport);
|
|
415
|
+
} catch (error) {
|
|
416
|
+
if (error instanceof GmailTransportError && error.code === 'invalid_grant') {
|
|
417
|
+
return refusal(400, 'code-rejected', "Microsoft's sign-in expired or was already used. Connect the mailbox again.");
|
|
418
|
+
}
|
|
419
|
+
if (error instanceof GmailTransportError && error.code === 'client_misconfigured') {
|
|
420
|
+
return refusal(503, 'not-configured', "Microsoft refused this deployment's app registration.");
|
|
421
|
+
}
|
|
422
|
+
if (error instanceof GmailTransportError && error.code === 'insufficient_scope') return scopesMissing();
|
|
423
|
+
return googleFailure(error, 'connect/complete Microsoft token exchange');
|
|
424
|
+
}
|
|
425
|
+
if (!grant.refreshToken) {
|
|
426
|
+
return refusal(422, 'refresh-token-missing', 'Microsoft did not grant offline access, so the mailbox could not send later. Connect it again.');
|
|
427
|
+
}
|
|
428
|
+
if (!microsoftScopesInclude(grant.scope)) return scopesMissing();
|
|
429
|
+
const identity = readMicrosoftIdToken(grant.idToken, {
|
|
430
|
+
clientId: config.config.clientId,
|
|
431
|
+
nonce: outreachOidcNonce(claims.nonce),
|
|
432
|
+
nowMs
|
|
433
|
+
});
|
|
434
|
+
if (identity.ok === false) {
|
|
435
|
+
return refusal(422, 'identity-unverified', "Microsoft's answer could not be verified. Connect the mailbox again.");
|
|
436
|
+
}
|
|
437
|
+
const client = createGraphClient(_extends({}, deps.transport, {
|
|
438
|
+
clientId: config.config.clientId,
|
|
439
|
+
clientSecret: config.config.clientSecret,
|
|
440
|
+
tenant: config.config.tenant,
|
|
441
|
+
refreshToken: grant.refreshToken,
|
|
442
|
+
accessToken: {
|
|
443
|
+
token: grant.accessToken,
|
|
444
|
+
expiresInSeconds: grant.expiresInSeconds
|
|
445
|
+
}
|
|
446
|
+
}));
|
|
447
|
+
let profile;
|
|
448
|
+
try {
|
|
449
|
+
profile = await client.getProfile();
|
|
450
|
+
} catch (error) {
|
|
451
|
+
if (error instanceof GmailTransportError && error.reconnectRequired) return scopesMissing();
|
|
452
|
+
return googleFailure(error, 'connect/complete Microsoft profile');
|
|
453
|
+
}
|
|
454
|
+
if (profile.id !== identity.identity.oid || !profile.emailAddress) {
|
|
455
|
+
return refusal(422, 'account-mismatch', 'The Microsoft 365 mailbox and the Microsoft sign-in did not match. Connect again.');
|
|
456
|
+
}
|
|
457
|
+
return {
|
|
458
|
+
provider: 'microsoft',
|
|
459
|
+
providerAccountId: identity.identity.sub,
|
|
460
|
+
email: profile.emailAddress,
|
|
461
|
+
sendAsOptions: [
|
|
462
|
+
{
|
|
463
|
+
email: profile.emailAddress,
|
|
464
|
+
displayName: profile.displayName,
|
|
465
|
+
isPrimary: true,
|
|
466
|
+
isDefault: true
|
|
467
|
+
}
|
|
468
|
+
],
|
|
469
|
+
scopes: grant.scope.split(/\s+/).filter(Boolean),
|
|
470
|
+
refreshToken: grant.refreshToken,
|
|
471
|
+
keyring: config.config.keyring
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* The connect's end, whichever provider named the account: the member's
|
|
476
|
+
* mailbox limit, the mailbox (new, or a reconnect that keeps its
|
|
477
|
+
* settings), its sealed grant, the aliases the provider confirmed, and the
|
|
478
|
+
* activity row.
|
|
479
|
+
*/ async function storeConnectedMailbox(input) {
|
|
480
|
+
var _aliases_confirmed;
|
|
481
|
+
const { gate, body, account, nowMs } = input;
|
|
482
|
+
const { provider, email: profileEmail, sendAsOptions } = account;
|
|
483
|
+
const firestore = deps.firestore();
|
|
484
|
+
const mailboxId = outreachMailboxId(gate.orgId, gate.uid, account.providerAccountId, provider);
|
|
304
485
|
const mailboxDoc = mailboxRef(firestore, gate.orgId, mailboxId);
|
|
305
486
|
const credentialDoc = mailboxCredentialsRef(firestore, mailboxId);
|
|
306
487
|
const [existingMailbox, existingCredential] = await Promise.all([
|
|
@@ -321,11 +502,11 @@ export function createOutreachMailboxRoutes(deps) {
|
|
|
321
502
|
var _ref;
|
|
322
503
|
const keepSendAs = sendAsOptions.some((option)=>option.email === current.sendAs);
|
|
323
504
|
reconnected = {
|
|
324
|
-
provider
|
|
505
|
+
provider,
|
|
325
506
|
email: profileEmail,
|
|
326
507
|
sendAs: keepSendAs ? current.sendAs : (_ref = defaultOption == null ? void 0 : defaultOption.email) != null ? _ref : profileEmail,
|
|
327
508
|
sendAsOptions,
|
|
328
|
-
// A reconnect restores a mailbox
|
|
509
|
+
// A reconnect restores a mailbox the provider had cut off, and leaves a
|
|
329
510
|
// mailbox its member paused paused.
|
|
330
511
|
status: current.status === 'paused' ? 'paused' : 'connected',
|
|
331
512
|
connectedAtMs: nowMs,
|
|
@@ -338,7 +519,7 @@ export function createOutreachMailboxRoutes(deps) {
|
|
|
338
519
|
const displayName = validateDisplayName((defaultOption == null ? void 0 : defaultOption.displayName) || gate.name || '');
|
|
339
520
|
mailbox = {
|
|
340
521
|
id: mailboxId,
|
|
341
|
-
provider
|
|
522
|
+
provider,
|
|
342
523
|
email: profileEmail,
|
|
343
524
|
sendAs: (_ref1 = defaultOption == null ? void 0 : defaultOption.email) != null ? _ref1 : profileEmail,
|
|
344
525
|
sendAsOptions,
|
|
@@ -372,12 +553,12 @@ export function createOutreachMailboxRoutes(deps) {
|
|
|
372
553
|
id: mailboxId,
|
|
373
554
|
orgId: gate.orgId,
|
|
374
555
|
mailboxId,
|
|
375
|
-
provider
|
|
556
|
+
provider,
|
|
376
557
|
connectedByUid: gate.uid,
|
|
377
|
-
providerAccountId:
|
|
558
|
+
providerAccountId: account.providerAccountId,
|
|
378
559
|
email: profileEmail,
|
|
379
|
-
scopes:
|
|
380
|
-
}, sealMailboxRefreshToken(
|
|
560
|
+
scopes: account.scopes
|
|
561
|
+
}, sealMailboxRefreshToken(account.refreshToken, mailboxId, account.keyring), {
|
|
381
562
|
createdAtMs: Number(existingCredential.exists ? existingCredential.get('createdAtMs') : 0) || nowMs,
|
|
382
563
|
updatedAtMs: nowMs
|
|
383
564
|
});
|
|
@@ -416,7 +597,7 @@ export function createOutreachMailboxRoutes(deps) {
|
|
|
416
597
|
created: !current,
|
|
417
598
|
confirmedAliases: aliases.ok ? (_aliases_confirmed = aliases.confirmed) != null ? _aliases_confirmed : [] : []
|
|
418
599
|
});
|
|
419
|
-
}
|
|
600
|
+
}
|
|
420
601
|
const settings = async (request)=>{
|
|
421
602
|
if (request.method !== 'POST') return methodNotAllowed('POST');
|
|
422
603
|
const body = await readBody(request);
|
|
@@ -429,7 +610,7 @@ export function createOutreachMailboxRoutes(deps) {
|
|
|
429
610
|
var _mailbox_sendAsOptions;
|
|
430
611
|
const sendAs = typeof body['sendAs'] === 'string' ? body['sendAs'].trim().toLowerCase() : '';
|
|
431
612
|
if (!((_mailbox_sendAsOptions = mailbox.sendAsOptions) != null ? _mailbox_sendAsOptions : []).some((option)=>option.email === sendAs)) {
|
|
432
|
-
return invalid('Choose one of the addresses
|
|
613
|
+
return invalid('Choose one of the addresses the provider has verified for this account.');
|
|
433
614
|
}
|
|
434
615
|
update.sendAs = sendAs;
|
|
435
616
|
}
|
|
@@ -527,7 +708,8 @@ export function createOutreachMailboxRoutes(deps) {
|
|
|
527
708
|
if (mailbox.status === 'reconnect_required' || mailbox.status === 'disconnected') {
|
|
528
709
|
return refusal(409, 'reconnect-required', 'Reconnect this mailbox before sending from it.');
|
|
529
710
|
}
|
|
530
|
-
|
|
711
|
+
const provider = mailbox.provider === 'microsoft' ? 'microsoft' : 'google';
|
|
712
|
+
if (!providerConfigured(provider)) return notConfigured(provider);
|
|
531
713
|
const limited = await deps.consumeRateLimit(`outreach-mailbox-test:${mailbox.id}`, {
|
|
532
714
|
limit: OUTREACH_TEST_SENDS_PER_HOUR,
|
|
533
715
|
windowMs: 60 * 60 * 1000
|
|
@@ -537,7 +719,7 @@ export function createOutreachMailboxRoutes(deps) {
|
|
|
537
719
|
}
|
|
538
720
|
/*
|
|
539
721
|
* To the member's own address unless they name another (AGL-3228). A
|
|
540
|
-
* test to oneself never leaves
|
|
722
|
+
* test to oneself never leaves the provider, so it arrives with no
|
|
541
723
|
* Authentication-Results at all and cannot show whether the send-as
|
|
542
724
|
* domain's DKIM and DMARC hold; an outside mailbox the member can read
|
|
543
725
|
* is the only place that can be seen. The rate limit above is the
|
|
@@ -554,10 +736,11 @@ export function createOutreachMailboxRoutes(deps) {
|
|
|
554
736
|
mailboxId: mailbox.id
|
|
555
737
|
}, _extends({}, deps.transport, {
|
|
556
738
|
readConfig: deps.readConfig,
|
|
739
|
+
readMicrosoftConfig,
|
|
557
740
|
now: deps.now
|
|
558
741
|
}));
|
|
559
742
|
if (opened.ok === false) {
|
|
560
|
-
if (opened.reason === 'not-configured') return notConfigured();
|
|
743
|
+
if (opened.reason === 'not-configured') return notConfigured(provider);
|
|
561
744
|
await markOutreachMailboxReconnectRequired(firestore, {
|
|
562
745
|
orgId: gate.orgId,
|
|
563
746
|
mailboxId: mailbox.id,
|
|
@@ -598,7 +781,7 @@ export function createOutreachMailboxRoutes(deps) {
|
|
|
598
781
|
errorCode: error.code,
|
|
599
782
|
nowMs
|
|
600
783
|
});
|
|
601
|
-
return refusal(409, 'reconnect-required',
|
|
784
|
+
return refusal(409, 'reconnect-required', `${providerName(provider)} refused this mailbox’s access. Connect it again.`);
|
|
602
785
|
}
|
|
603
786
|
if (error instanceof Rfc5322MessageError) {
|
|
604
787
|
return refusal(400, 'invalid-settings', `The test could not be written: ${error.message}`);
|
|
@@ -649,7 +832,7 @@ export function createOutreachMailboxRoutes(deps) {
|
|
|
649
832
|
const mailbox = readMailbox(mailboxId, snapshot.exists ? snapshot.data() : undefined);
|
|
650
833
|
if (!mailbox) return refusal(404, 'mailbox-not-found', 'That mailbox is not connected to this organization.');
|
|
651
834
|
const sendAs = mailbox.sendAs || mailbox.email;
|
|
652
|
-
const expectation = googleMailboxSenderExpectation(sendAs);
|
|
835
|
+
const expectation = mailbox.provider === 'microsoft' ? microsoftMailboxSenderExpectation(sendAs) : googleMailboxSenderExpectation(sendAs);
|
|
653
836
|
const read = expectation ? await deps.readSenderReadiness(expectation, {
|
|
654
837
|
fresh: body['fresh'] === true
|
|
655
838
|
}).catch(()=>null) : null;
|