@oxygen-agent/cli 1.677.10 → 1.681.2
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 +1 -1
- package/dist/index.js +22 -426
- package/node_modules/@oxygen/shared/dist/file-import.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/file-import.js +16 -13
- package/node_modules/@oxygen/shared/dist/mailbox-import.d.ts +58 -0
- package/node_modules/@oxygen/shared/dist/mailbox-import.js +556 -0
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +1 -1
- package/node_modules/@oxygen/shared/package.json +5 -0
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/index.js
CHANGED
|
@@ -11,7 +11,8 @@ import { applyOxygenHelp } from "./help.js";
|
|
|
11
11
|
import { buildCommandManifest, getCommandManifestEntry, searchCommandManifest, suggestCommandNames, } from "./command-manifest.js";
|
|
12
12
|
import { AGENCY_DIRECTORY_REGIONS, AGENCY_DIRECTORY_SERVICES, COLLAB_GATE_KINDS, COLLAB_GATE_PROSE, COLLAB_SUBJECT_KINDS, COLLAB_SUBJECT_KINDS_PROSE, COLLAB_SUBJECT_LABELS, COLLAB_SUBJECT_PROSE, GATE_KIND_SUBJECTS, describeWorkflowStatusChange, formatCellForDisplay, formatPublicBudgetScopes, SUBJECT_PATH_FORMS_PROSE, formatSubjectPath, exitCodeForOxygenError, parseSubjectPath, parseSubjectRef, parseWorkflowStatusChange, isVersionGreater, isVersionLess, MAX_MCP_TOOL_NAME_LENGTH, OXYGEN_CAPABILITY_ROUTES, OXYGEN_VERSION, OxygenError, getCapabilityRouteMatch, inferCapabilityRoute, parseKnowledgePageMarkdown, PLAN_LIMITS, serializeCapabilityRoute, sleep, success, TAG_KINDS_PROSE, toFailure, workflowMcpToolName, } from "@oxygen/shared";
|
|
13
13
|
import { TAG_COLORS } from "@oxygen/shared/select-options";
|
|
14
|
-
import { inferImportColumnLabels, inferRowsFileFormat, normalizeImportColumnKey, normalizeRowsForNewTable, normalizeRowsFormat, parseRowsFileBuffer, } from "@oxygen/shared/file-import";
|
|
14
|
+
import { inferImportColumnLabels, inferRowsFileFormat, normalizeImportColumnKey, normalizeRowsForNewTable, normalizeRowsFormat, parseRowsFileBuffer, parseXlsxWorkbookBuffer, } from "@oxygen/shared/file-import";
|
|
15
|
+
import { MAILBOX_IMPORT_FILE_MAX_BYTES as SHARED_MAILBOX_IMPORT_FILE_MAX_BYTES, MAILBOX_IMPORT_ROW_LIMIT as SHARED_MAILBOX_IMPORT_ROW_LIMIT, normalizeMailboxImportFile as normalizeSharedMailboxImportFile, normalizeMailboxImportVendor as normalizeSharedMailboxImportVendor, normalizeMailboxWorkbookRows, parseMailboxImportText, summarizeMailboxImportValidation as summarizeSharedMailboxImportValidation, } from "@oxygen/shared/mailbox-import";
|
|
15
16
|
import { assertRecipeBundleSafe, assertWorkflowGraphManifest, assertWorkflowManifest, buildRecipeManifest, compileWorkflowDefinition, isAnyWorkflowManifest, isRecipeManifest, isWorkflowDefinition, isWorkflowGraphManifest, isWorkflowManifest, } from "@oxygen/workflows";
|
|
16
17
|
import { isRecipeDefinition } from "@oxygen/recipe-sdk";
|
|
17
18
|
import { createBrowserLoginSession, createRelayLoginSession, openBrowser } from "./browser-login.js";
|
|
@@ -894,413 +895,26 @@ function readJsonFileValue(path, inputName) {
|
|
|
894
895
|
* shortcut) so the server applies the encrypted transfer-vault contract.
|
|
895
896
|
*/
|
|
896
897
|
function normalizeMailboxImportFile(value, mode) {
|
|
897
|
-
|
|
898
|
-
|
|
898
|
+
try {
|
|
899
|
+
return normalizeSharedMailboxImportFile(value, mode);
|
|
899
900
|
}
|
|
900
|
-
|
|
901
|
-
if (
|
|
902
|
-
|
|
901
|
+
catch (error) {
|
|
902
|
+
if (error instanceof OxygenError &&
|
|
903
|
+
error.message ===
|
|
904
|
+
"This identity-only import contains a password. Choose a secure credential import for a compatible Google app-password export.") {
|
|
905
|
+
throw new OxygenError("invalid_request", "Mailbox credentials require --from credentials --vendor <source> (or --from hypertide for a Hypertide export); keep --validate-only for a no-network preflight. An identity import never accepts or forwards passwords.", { exitCode: 1 });
|
|
903
906
|
}
|
|
904
|
-
|
|
905
|
-
}
|
|
906
|
-
return dedupeMailboxImportRows(rows);
|
|
907
|
+
throw error;
|
|
908
|
+
}
|
|
907
909
|
}
|
|
908
910
|
function summarizeMailboxImportValidation(mailboxes, input) {
|
|
909
|
-
const providers = { google: 0, microsoft: 0 };
|
|
910
|
-
const infrastructurePlatforms = {};
|
|
911
|
-
let credentialRows = 0;
|
|
912
|
-
for (const mailbox of mailboxes) {
|
|
913
|
-
if (mailbox.provider === "google")
|
|
914
|
-
providers.google += 1;
|
|
915
|
-
if (mailbox.provider === "microsoft")
|
|
916
|
-
providers.microsoft += 1;
|
|
917
|
-
if (typeof mailbox.infrastructure_platform === "string") {
|
|
918
|
-
const platform = mailbox.infrastructure_platform;
|
|
919
|
-
infrastructurePlatforms[platform] =
|
|
920
|
-
(infrastructurePlatforms[platform] ?? 0) + 1;
|
|
921
|
-
}
|
|
922
|
-
if (typeof mailbox.app_password === "string")
|
|
923
|
-
credentialRows += 1;
|
|
924
|
-
}
|
|
925
911
|
return {
|
|
926
|
-
|
|
927
|
-
source: input.source,
|
|
928
|
-
...(input.sourceProvider
|
|
929
|
-
? { source_provider: input.sourceProvider }
|
|
930
|
-
: {}),
|
|
931
|
-
rows: mailboxes.length,
|
|
932
|
-
providers,
|
|
933
|
-
infrastructure_platforms: infrastructurePlatforms,
|
|
934
|
-
credential_rows: credentialRows,
|
|
935
|
-
identity_only_rows: mailboxes.length - credentialRows,
|
|
936
|
-
limits: {
|
|
937
|
-
max_rows: MAILBOX_IMPORT_ROW_LIMIT,
|
|
938
|
-
max_file_bytes: MAILBOX_IMPORT_FILE_MAX_BYTES,
|
|
939
|
-
},
|
|
940
|
-
mutation: false,
|
|
941
|
-
network_request: false,
|
|
942
|
-
provider_call: false,
|
|
943
|
-
credits_used: 0,
|
|
912
|
+
...summarizeSharedMailboxImportValidation(mailboxes, input),
|
|
944
913
|
next_action: "Re-run the same command without --validate-only to register these mailboxes.",
|
|
945
914
|
};
|
|
946
915
|
}
|
|
947
|
-
const MAILBOX_EMAIL_HEADERS = new Set([
|
|
948
|
-
"address",
|
|
949
|
-
"email",
|
|
950
|
-
"emailaddress",
|
|
951
|
-
"fromemail",
|
|
952
|
-
"mailbox",
|
|
953
|
-
"mailboxaddress",
|
|
954
|
-
"mailboxemail",
|
|
955
|
-
]);
|
|
956
|
-
const MAILBOX_PROVIDER_HEADERS = new Set([
|
|
957
|
-
"emailprovider",
|
|
958
|
-
"esp",
|
|
959
|
-
"mailboxprovider",
|
|
960
|
-
"platform",
|
|
961
|
-
"provider",
|
|
962
|
-
"serviceprovider",
|
|
963
|
-
"type",
|
|
964
|
-
]);
|
|
965
|
-
const MAILBOX_PASSWORD_HEADERS = new Set([
|
|
966
|
-
"apppassword",
|
|
967
|
-
"applicationpassword",
|
|
968
|
-
"googleapppassword",
|
|
969
|
-
"imappassword",
|
|
970
|
-
"mailboxpassword",
|
|
971
|
-
"password",
|
|
972
|
-
"smtppassword",
|
|
973
|
-
]);
|
|
974
|
-
const MAILBOX_WORKSPACE_HEADERS = new Set([
|
|
975
|
-
"externalaccountid",
|
|
976
|
-
"mailboxid",
|
|
977
|
-
"mailboxuid",
|
|
978
|
-
"sourceaccountid",
|
|
979
|
-
"sourcemailboxid",
|
|
980
|
-
"uid",
|
|
981
|
-
"workspaceexternalid",
|
|
982
|
-
]);
|
|
983
|
-
const MAILBOX_PLATFORM_HEADERS = new Set([
|
|
984
|
-
"infrastructure",
|
|
985
|
-
"infrastructureplatform",
|
|
986
|
-
"infraplatform",
|
|
987
|
-
]);
|
|
988
|
-
const MAILBOX_TENANT_HEADERS = new Set([
|
|
989
|
-
"azuretenantid",
|
|
990
|
-
"entratenantid",
|
|
991
|
-
"microsofttenantid",
|
|
992
|
-
"tenantid",
|
|
993
|
-
]);
|
|
994
|
-
const MAILBOX_NON_TRANSFERABLE_SECRET_HEADERS = new Set([
|
|
995
|
-
"accesstoken",
|
|
996
|
-
"applicationsecret",
|
|
997
|
-
"authenticatorsecret",
|
|
998
|
-
"authorization",
|
|
999
|
-
"authorizationcode",
|
|
1000
|
-
"bearertoken",
|
|
1001
|
-
"clientsecret",
|
|
1002
|
-
"clientprivatekey",
|
|
1003
|
-
"delegationkey",
|
|
1004
|
-
"idtoken",
|
|
1005
|
-
"mfacode",
|
|
1006
|
-
"mfasecret",
|
|
1007
|
-
"oauthaccesstoken",
|
|
1008
|
-
"oauthcode",
|
|
1009
|
-
"oauthrefreshtoken",
|
|
1010
|
-
"oauthtoken",
|
|
1011
|
-
"onetimepassword",
|
|
1012
|
-
"otp",
|
|
1013
|
-
"otpsecret",
|
|
1014
|
-
"privatekey",
|
|
1015
|
-
"refreshtoken",
|
|
1016
|
-
"serviceaccountkey",
|
|
1017
|
-
"serviceaccountjson",
|
|
1018
|
-
"serviceaccountprivatekey",
|
|
1019
|
-
"totp",
|
|
1020
|
-
"totpsecret",
|
|
1021
|
-
]);
|
|
1022
|
-
const MAILBOX_IMPORT_FILE_MAX_BYTES = 5 * 1024 * 1024;
|
|
1023
|
-
const MAILBOX_IMPORT_ROW_LIMIT = 500;
|
|
1024
|
-
/**
|
|
1025
|
-
* Normalize common mailbox-vendor export labels locally, then send only
|
|
1026
|
-
* Oxygen's canonical fields. Passwords are accepted only in credential mode
|
|
1027
|
-
* and only become Google app passwords; Microsoft passwords are dropped because
|
|
1028
|
-
* one Outlook OAuth consent per mailbox is its only warmup path. OAuth grants,
|
|
1029
|
-
* MFA/TOTP seeds, and delegation keys are rejected in every mode. Host columns
|
|
1030
|
-
* may prove the provider but never cross the request boundary.
|
|
1031
|
-
*/
|
|
1032
|
-
function normalizeMailboxExportRow(row, index, mode) {
|
|
1033
|
-
const byHeader = new Map();
|
|
1034
|
-
for (const [header, value] of Object.entries(row)) {
|
|
1035
|
-
const normalizedHeader = normalizeMailboxExportHeader(header);
|
|
1036
|
-
const existing = byHeader.get(normalizedHeader);
|
|
1037
|
-
if (existing !== undefined &&
|
|
1038
|
-
String(existing).trim() !== String(value ?? "").trim()) {
|
|
1039
|
-
throw new OxygenError("invalid_request", `mailboxes[${index}] contains conflicting columns that normalize to ${normalizedHeader}.`, { exitCode: 1 });
|
|
1040
|
-
}
|
|
1041
|
-
byHeader.set(normalizedHeader, value);
|
|
1042
|
-
}
|
|
1043
|
-
const email = readUniqueMailboxExportString(byHeader, MAILBOX_EMAIL_HEADERS, index, "email_address", (value) => value.trim().toLowerCase()) ?? readEmailShapedMailboxUsername(byHeader);
|
|
1044
|
-
const providerValues = readMailboxExportStrings(byHeader, MAILBOX_PROVIDER_HEADERS);
|
|
1045
|
-
const provider = normalizeMailboxProvider(providerValues, byHeader, index);
|
|
1046
|
-
if (!email ||
|
|
1047
|
-
email.length > 320 ||
|
|
1048
|
-
!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
1049
|
-
throw new OxygenError("invalid_request", `mailboxes[${index}].email_address must be a valid address of at most 320 characters.`, { exitCode: 1 });
|
|
1050
|
-
}
|
|
1051
|
-
if (!provider) {
|
|
1052
|
-
throw new OxygenError("invalid_request", `mailboxes[${index}].provider could not be inferred. Oxygen imports Google Workspace and Microsoft 365/Entra identities; generic SMTP credentials are not transferable yet.`, { exitCode: 1 });
|
|
1053
|
-
}
|
|
1054
|
-
if (hasMailboxExportValue(byHeader, MAILBOX_NON_TRANSFERABLE_SECRET_HEADERS) ||
|
|
1055
|
-
hasNonTransferableMailboxAuthLikeValue(byHeader)) {
|
|
1056
|
-
throw new OxygenError("invalid_request", `mailboxes[${index}] contains OAuth, MFA/TOTP, client-secret, or delegation material. Those credentials are never transferable; import the mailbox identity and complete the provider's administrator consent flow.`, { exitCode: 1 });
|
|
1057
|
-
}
|
|
1058
|
-
const passwords = readMailboxExportStrings(byHeader, MAILBOX_PASSWORD_HEADERS);
|
|
1059
|
-
const distinctPasswords = [...new Set(passwords)];
|
|
1060
|
-
if (distinctPasswords.some((password) => password.length > 1024)) {
|
|
1061
|
-
throw new OxygenError("invalid_request", `mailboxes[${index}].app_password must be at most 1024 characters.`, { exitCode: 1 });
|
|
1062
|
-
}
|
|
1063
|
-
if (mode === "identity" && distinctPasswords.length > 0) {
|
|
1064
|
-
throw new OxygenError("invalid_request", "Mailbox credentials require --from credentials --vendor <source> (or --from hypertide for a Hypertide export); keep --validate-only for a no-network preflight. An identity import never accepts or forwards passwords.", { exitCode: 1 });
|
|
1065
|
-
}
|
|
1066
|
-
if (provider !== "microsoft" && distinctPasswords.length > 1) {
|
|
1067
|
-
throw new OxygenError("invalid_request", `mailboxes[${index}] contains conflicting SMTP/IMAP/app-password values.`, { exitCode: 1 });
|
|
1068
|
-
}
|
|
1069
|
-
if (mode === "credential" &&
|
|
1070
|
-
provider === "google" &&
|
|
1071
|
-
distinctPasswords.length === 0) {
|
|
1072
|
-
throw new OxygenError("invalid_request", `mailboxes[${index}] needs a Google app password for credential import. OAuth tokens and an ordinary account password are not portable.`, { exitCode: 1 });
|
|
1073
|
-
}
|
|
1074
|
-
const workspaceExternalId = readUniqueMailboxExportString(byHeader, MAILBOX_WORKSPACE_HEADERS, index, "workspace_external_id", (value) => value.trim());
|
|
1075
|
-
if (workspaceExternalId && workspaceExternalId.length > 512) {
|
|
1076
|
-
throw new OxygenError("invalid_request", `mailboxes[${index}].workspace_external_id must be at most 512 characters.`, { exitCode: 1 });
|
|
1077
|
-
}
|
|
1078
|
-
const tenantId = readUniqueMailboxExportString(byHeader, MAILBOX_TENANT_HEADERS, index, "tenant_id", (value) => value.trim().toLowerCase());
|
|
1079
|
-
if (tenantId && provider !== "microsoft") {
|
|
1080
|
-
throw new OxygenError("invalid_request", `mailboxes[${index}].tenant_id applies only to Microsoft mailboxes.`, { exitCode: 1 });
|
|
1081
|
-
}
|
|
1082
|
-
if (tenantId &&
|
|
1083
|
-
!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(tenantId.trim())) {
|
|
1084
|
-
throw new OxygenError("invalid_request", `mailboxes[${index}].tenant_id must be a Microsoft Entra tenant GUID.`, { exitCode: 1 });
|
|
1085
|
-
}
|
|
1086
|
-
const infrastructurePlatformRaw = readUniqueMailboxExportString(byHeader, MAILBOX_PLATFORM_HEADERS, index, "infrastructure_platform", (value) => normalizeMailboxExportHeader(value)) ??
|
|
1087
|
-
(providerValues.some((value) => ["azure", "entra", "microsoftazure"].includes(normalizeMailboxExportHeader(value)))
|
|
1088
|
-
? "microsoft_azure"
|
|
1089
|
-
: null);
|
|
1090
|
-
const infrastructurePlatform = normalizeMailboxInfrastructurePlatform(infrastructurePlatformRaw, provider, index);
|
|
1091
|
-
return {
|
|
1092
|
-
email_address: email.trim().toLowerCase(),
|
|
1093
|
-
provider,
|
|
1094
|
-
...(workspaceExternalId !== null
|
|
1095
|
-
? { workspace_external_id: workspaceExternalId.trim() }
|
|
1096
|
-
: {}),
|
|
1097
|
-
...(infrastructurePlatformRaw
|
|
1098
|
-
? { infrastructure_platform: infrastructurePlatform }
|
|
1099
|
-
: {}),
|
|
1100
|
-
...(tenantId ? { tenant_id: tenantId.trim().toLowerCase() } : {}),
|
|
1101
|
-
...(mode === "credential" &&
|
|
1102
|
-
provider === "google" &&
|
|
1103
|
-
distinctPasswords[0] !== undefined
|
|
1104
|
-
? { app_password: distinctPasswords[0] }
|
|
1105
|
-
: {}),
|
|
1106
|
-
};
|
|
1107
|
-
}
|
|
1108
|
-
function normalizeMailboxExportHeader(value) {
|
|
1109
|
-
return value
|
|
1110
|
-
.replace(/^\uFEFF/, "")
|
|
1111
|
-
.normalize("NFKC")
|
|
1112
|
-
.trim()
|
|
1113
|
-
.toLowerCase()
|
|
1114
|
-
.replace(/[^a-z0-9]/g, "");
|
|
1115
|
-
}
|
|
1116
|
-
function readMailboxExportString(byHeader, headers) {
|
|
1117
|
-
for (const header of headers) {
|
|
1118
|
-
const value = byHeader.get(header);
|
|
1119
|
-
if (typeof value === "string" && value.length > 0)
|
|
1120
|
-
return value;
|
|
1121
|
-
}
|
|
1122
|
-
return null;
|
|
1123
|
-
}
|
|
1124
|
-
function readUniqueMailboxExportString(byHeader, headers, index, field, normalize) {
|
|
1125
|
-
const values = readMailboxExportStrings(byHeader, headers);
|
|
1126
|
-
if (values.length === 0)
|
|
1127
|
-
return null;
|
|
1128
|
-
const normalized = [...new Set(values.map(normalize))];
|
|
1129
|
-
if (normalized.length > 1) {
|
|
1130
|
-
throw new OxygenError("invalid_request", `mailboxes[${index}] contains conflicting ${field} aliases.`, { exitCode: 1 });
|
|
1131
|
-
}
|
|
1132
|
-
return normalized[0] ?? null;
|
|
1133
|
-
}
|
|
1134
|
-
function readMailboxExportStrings(byHeader, headers) {
|
|
1135
|
-
const values = [];
|
|
1136
|
-
for (const header of headers) {
|
|
1137
|
-
const value = byHeader.get(header);
|
|
1138
|
-
if (typeof value === "string" && value.length > 0)
|
|
1139
|
-
values.push(value);
|
|
1140
|
-
}
|
|
1141
|
-
return values;
|
|
1142
|
-
}
|
|
1143
|
-
function hasMailboxExportValue(byHeader, headers) {
|
|
1144
|
-
for (const header of headers) {
|
|
1145
|
-
const value = byHeader.get(header);
|
|
1146
|
-
if (value !== undefined && value !== null && String(value).trim()) {
|
|
1147
|
-
return true;
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1150
|
-
return false;
|
|
1151
|
-
}
|
|
1152
|
-
function hasNonTransferableMailboxAuthLikeValue(byHeader) {
|
|
1153
|
-
for (const [header, value] of byHeader) {
|
|
1154
|
-
if (/(?:token|secret|privatekey|totp|passcode|authorization)/.test(header) &&
|
|
1155
|
-
value !== undefined &&
|
|
1156
|
-
value !== null &&
|
|
1157
|
-
String(value).trim()) {
|
|
1158
|
-
return true;
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
|
-
return false;
|
|
1162
|
-
}
|
|
1163
|
-
function readEmailShapedMailboxUsername(byHeader) {
|
|
1164
|
-
for (const header of ["username", "imapusername", "smtpusername"]) {
|
|
1165
|
-
const value = byHeader.get(header);
|
|
1166
|
-
if (typeof value === "string" && value.includes("@"))
|
|
1167
|
-
return value.trim().toLowerCase();
|
|
1168
|
-
}
|
|
1169
|
-
return null;
|
|
1170
|
-
}
|
|
1171
|
-
function normalizeMailboxProvider(providerValues, byHeader, index) {
|
|
1172
|
-
const explicit = providerValues.map((value) => {
|
|
1173
|
-
const normalized = normalizeMailboxExportHeader(value);
|
|
1174
|
-
if (["google", "googleworkspace", "gmail", "gsuite"].includes(normalized)) {
|
|
1175
|
-
return "google";
|
|
1176
|
-
}
|
|
1177
|
-
if ([
|
|
1178
|
-
"azure",
|
|
1179
|
-
"entra",
|
|
1180
|
-
"m365",
|
|
1181
|
-
"microsoft",
|
|
1182
|
-
"microsoft365",
|
|
1183
|
-
"microsoftazure",
|
|
1184
|
-
"ms365",
|
|
1185
|
-
"o365",
|
|
1186
|
-
"office365",
|
|
1187
|
-
"outlook",
|
|
1188
|
-
].includes(normalized)) {
|
|
1189
|
-
return "microsoft";
|
|
1190
|
-
}
|
|
1191
|
-
throw new OxygenError("invalid_request", `mailboxes[${index}].provider is unsupported; expected Google/Gmail/Workspace or Microsoft/Outlook/Office 365/Entra.`, { exitCode: 1 });
|
|
1192
|
-
});
|
|
1193
|
-
const explicitKinds = [...new Set(explicit)];
|
|
1194
|
-
if (explicitKinds.length > 1) {
|
|
1195
|
-
throw new OxygenError("invalid_request", `mailboxes[${index}] contains conflicting provider aliases.`, { exitCode: 1 });
|
|
1196
|
-
}
|
|
1197
|
-
const hostValues = [
|
|
1198
|
-
byHeader.get("imaphost"),
|
|
1199
|
-
byHeader.get("imaphostname"),
|
|
1200
|
-
byHeader.get("imapserver"),
|
|
1201
|
-
byHeader.get("smtphost"),
|
|
1202
|
-
byHeader.get("smtphostname"),
|
|
1203
|
-
byHeader.get("smtpserver"),
|
|
1204
|
-
].filter((value) => typeof value === "string" && Boolean(value.trim()));
|
|
1205
|
-
const hostKinds = hostValues.map((value) => {
|
|
1206
|
-
const hostname = normalizeMailboxHost(value);
|
|
1207
|
-
if (hostname === "smtp.gmail.com" || hostname === "imap.gmail.com") {
|
|
1208
|
-
return "google";
|
|
1209
|
-
}
|
|
1210
|
-
if (hostname === "smtp.office365.com" ||
|
|
1211
|
-
hostname === "outlook.office365.com") {
|
|
1212
|
-
return "microsoft";
|
|
1213
|
-
}
|
|
1214
|
-
throw new OxygenError("invalid_request", `mailboxes[${index}] contains a non-standard SMTP/IMAP host. Oxygen imports Google Workspace and Microsoft 365/Entra identities; generic SMTP credential transport is not supported.`, { exitCode: 1 });
|
|
1215
|
-
});
|
|
1216
|
-
const inferredKinds = [...new Set(hostKinds)];
|
|
1217
|
-
if (inferredKinds.length > 1) {
|
|
1218
|
-
throw new OxygenError("invalid_request", `mailboxes[${index}] contains conflicting Google and Microsoft mail hosts.`, { exitCode: 1 });
|
|
1219
|
-
}
|
|
1220
|
-
const explicitKind = explicitKinds[0];
|
|
1221
|
-
const inferredKind = inferredKinds[0];
|
|
1222
|
-
if (explicitKind && inferredKind && explicitKind !== inferredKind) {
|
|
1223
|
-
throw new OxygenError("invalid_request", `mailboxes[${index}] has conflicting provider and SMTP/IMAP host evidence.`, { exitCode: 1 });
|
|
1224
|
-
}
|
|
1225
|
-
return explicitKind ?? inferredKind ?? null;
|
|
1226
|
-
}
|
|
1227
|
-
function normalizeMailboxHost(value) {
|
|
1228
|
-
const withoutScheme = value
|
|
1229
|
-
.trim()
|
|
1230
|
-
.toLowerCase()
|
|
1231
|
-
.replace(/^[a-z]+:\/\//, "");
|
|
1232
|
-
return withoutScheme.split(/[/:]/, 1)[0] ?? "";
|
|
1233
|
-
}
|
|
1234
|
-
function normalizeMailboxInfrastructurePlatform(raw, provider, index) {
|
|
1235
|
-
if (!raw) {
|
|
1236
|
-
return provider === "google" ? "google_workspace" : "microsoft_365";
|
|
1237
|
-
}
|
|
1238
|
-
const normalized = normalizeMailboxExportHeader(raw);
|
|
1239
|
-
if (["google", "googleworkspace", "gmail", "gsuite", "workspace"].includes(normalized)) {
|
|
1240
|
-
if (provider !== "google") {
|
|
1241
|
-
throw mailboxPlatformConflict(index, provider);
|
|
1242
|
-
}
|
|
1243
|
-
return "google_workspace";
|
|
1244
|
-
}
|
|
1245
|
-
if (["azure", "entra", "microsoftazure"].includes(normalized)) {
|
|
1246
|
-
if (provider !== "microsoft") {
|
|
1247
|
-
throw mailboxPlatformConflict(index, provider);
|
|
1248
|
-
}
|
|
1249
|
-
return "microsoft_azure";
|
|
1250
|
-
}
|
|
1251
|
-
if ([
|
|
1252
|
-
"m365",
|
|
1253
|
-
"microsoft",
|
|
1254
|
-
"microsoft365",
|
|
1255
|
-
"ms365",
|
|
1256
|
-
"o365",
|
|
1257
|
-
"office365",
|
|
1258
|
-
"outlook",
|
|
1259
|
-
].includes(normalized)) {
|
|
1260
|
-
if (provider !== "microsoft") {
|
|
1261
|
-
throw mailboxPlatformConflict(index, provider);
|
|
1262
|
-
}
|
|
1263
|
-
return "microsoft_365";
|
|
1264
|
-
}
|
|
1265
|
-
throw new OxygenError("invalid_request", `mailboxes[${index}].infrastructure_platform is unsupported; expected Google Workspace, Microsoft 365, Azure, or Entra.`, { exitCode: 1 });
|
|
1266
|
-
}
|
|
1267
|
-
function mailboxPlatformConflict(index, provider) {
|
|
1268
|
-
return new OxygenError("invalid_request", `mailboxes[${index}].infrastructure_platform conflicts with provider=${provider}.`, { exitCode: 1 });
|
|
1269
|
-
}
|
|
1270
|
-
function dedupeMailboxImportRows(rows) {
|
|
1271
|
-
const unique = new Map();
|
|
1272
|
-
for (const row of rows) {
|
|
1273
|
-
const address = String(row.email_address).toLowerCase();
|
|
1274
|
-
const existing = unique.get(address);
|
|
1275
|
-
if (!existing) {
|
|
1276
|
-
unique.set(address, row);
|
|
1277
|
-
continue;
|
|
1278
|
-
}
|
|
1279
|
-
if (JSON.stringify(existing) !== JSON.stringify(row)) {
|
|
1280
|
-
throw new OxygenError("invalid_request", `Duplicate mailbox ${address} has conflicting provider, platform, tenant, external id, or credential fields.`, { exitCode: 1 });
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
return [...unique.values()];
|
|
1284
|
-
}
|
|
1285
916
|
function normalizeMailboxImportVendor(raw, from) {
|
|
1286
|
-
|
|
1287
|
-
if (raw && raw.trim().toLowerCase() !== "hypertide") {
|
|
1288
|
-
throw new OxygenError("invalid_request", "--from hypertide is fixed to --vendor hypertide.", { exitCode: 1 });
|
|
1289
|
-
}
|
|
1290
|
-
return null;
|
|
1291
|
-
}
|
|
1292
|
-
if (from === "credentials" && !raw) {
|
|
1293
|
-
throw new OxygenError("invalid_request", "--vendor <source> is required with --from credentials so the imported mailbox keeps its provenance.", { exitCode: 1 });
|
|
1294
|
-
}
|
|
1295
|
-
if (!raw)
|
|
1296
|
-
return null;
|
|
1297
|
-
const normalized = raw.trim().toLowerCase();
|
|
1298
|
-
if (normalized.length < 1 ||
|
|
1299
|
-
normalized.length > 64 ||
|
|
1300
|
-
!/^[a-z0-9]+(?:[._-][a-z0-9]+)*$/.test(normalized)) {
|
|
1301
|
-
throw new OxygenError("invalid_request", "--vendor must be 1-64 lowercase letters, digits, dots, underscores, or hyphens.", { exitCode: 1 });
|
|
1302
|
-
}
|
|
1303
|
-
return normalized;
|
|
917
|
+
return normalizeSharedMailboxImportVendor(raw, from);
|
|
1304
918
|
}
|
|
1305
919
|
async function readMailboxImportFile(path) {
|
|
1306
920
|
let buffer;
|
|
@@ -1310,42 +924,24 @@ async function readMailboxImportFile(path) {
|
|
|
1310
924
|
catch {
|
|
1311
925
|
throw new OxygenError("mailbox_import_file_unreadable", `Couldn't read --file '${basename(path)}'. Check that the path exists and is readable. CSV/JSON/JSONL/XLSX identity files are accepted; credential exports require --from credentials (or --from hypertide). See https://oxygen-agent.com/docs/providers/mailbox-compatibility.`, { exitCode: 1 });
|
|
1312
926
|
}
|
|
1313
|
-
if (buffer.byteLength >
|
|
1314
|
-
throw new OxygenError("invalid_request", `Mailbox import files must be ${
|
|
927
|
+
if (buffer.byteLength > SHARED_MAILBOX_IMPORT_FILE_MAX_BYTES) {
|
|
928
|
+
throw new OxygenError("invalid_request", `Mailbox import files must be ${SHARED_MAILBOX_IMPORT_FILE_MAX_BYTES / 1024 / 1024} MB or smaller.`, { exitCode: 1 });
|
|
1315
929
|
}
|
|
1316
930
|
const format = inferRowsFileFormat(path);
|
|
1317
931
|
let rows;
|
|
1318
|
-
if (format === "
|
|
1319
|
-
let parsed;
|
|
932
|
+
if (format === "xlsx") {
|
|
1320
933
|
try {
|
|
1321
|
-
|
|
934
|
+
rows = normalizeMailboxWorkbookRows(await parseXlsxWorkbookBuffer(buffer));
|
|
1322
935
|
}
|
|
1323
936
|
catch {
|
|
1324
|
-
throw new OxygenError("
|
|
1325
|
-
}
|
|
1326
|
-
if (Array.isArray(parsed)) {
|
|
1327
|
-
rows = parsed;
|
|
1328
|
-
}
|
|
1329
|
-
else if (parsed &&
|
|
1330
|
-
typeof parsed === "object" &&
|
|
1331
|
-
!Array.isArray(parsed) &&
|
|
1332
|
-
Array.isArray(parsed.mailboxes)) {
|
|
1333
|
-
rows = parsed.mailboxes;
|
|
1334
|
-
}
|
|
1335
|
-
else {
|
|
1336
|
-
throw new OxygenError("invalid_request", "--file JSON must contain a mailbox array or { mailboxes: [...] }.", { exitCode: 1 });
|
|
937
|
+
throw new OxygenError("invalid_mailbox_import_file", `Couldn't parse the ${format.toUpperCase()} mailbox file. File contents are omitted from this error because mailbox imports may contain credentials.`, { exitCode: 1 });
|
|
1337
938
|
}
|
|
1338
939
|
}
|
|
1339
940
|
else {
|
|
1340
|
-
|
|
1341
|
-
rows = await parseRowsFileBuffer(buffer, format);
|
|
1342
|
-
}
|
|
1343
|
-
catch {
|
|
1344
|
-
throw new OxygenError("invalid_mailbox_import_file", `Couldn't parse the ${format.toUpperCase()} mailbox file. File contents are omitted from this error because mailbox imports may contain credentials.`, { exitCode: 1 });
|
|
1345
|
-
}
|
|
941
|
+
rows = parseMailboxImportText(buffer.toString("utf8"), format);
|
|
1346
942
|
}
|
|
1347
|
-
if (rows.length >
|
|
1348
|
-
throw new OxygenError("invalid_request", `Mailbox imports are limited to ${
|
|
943
|
+
if (rows.length > SHARED_MAILBOX_IMPORT_ROW_LIMIT) {
|
|
944
|
+
throw new OxygenError("invalid_request", `Mailbox imports are limited to ${SHARED_MAILBOX_IMPORT_ROW_LIMIT} rows per request.`, { exitCode: 1 });
|
|
1349
945
|
}
|
|
1350
946
|
return rows;
|
|
1351
947
|
}
|
|
@@ -13169,7 +12765,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
13169
12765
|
: from === "credentials"
|
|
13170
12766
|
? "credential_file"
|
|
13171
12767
|
: "identity_file",
|
|
13172
|
-
sourceProvider,
|
|
12768
|
+
sourceProvider: from === "hypertide" ? "hypertide" : sourceProvider,
|
|
13173
12769
|
});
|
|
13174
12770
|
}
|
|
13175
12771
|
return requestOxygen("/api/cli/mailboxes", {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Row } from "read-excel-file/node";
|
|
1
2
|
import { type ImportColumnDataType } from "./column-types.js";
|
|
2
3
|
export type RowsFileFormat = "json" | "jsonl" | "csv" | "xlsx";
|
|
3
4
|
export type ImportTableColumn = {
|
|
@@ -16,6 +17,10 @@ export declare function normalizeRowsFormat(value: string | undefined, fallback:
|
|
|
16
17
|
export declare function parseRowsFileBuffer(buffer: Buffer, format: RowsFileFormat, options?: {
|
|
17
18
|
sheet?: string;
|
|
18
19
|
}): Promise<Record<string, unknown>[]>;
|
|
20
|
+
/** Read raw workbook cells for consumers with stricter, domain-specific headers. */
|
|
21
|
+
export declare function parseXlsxWorkbookBuffer(buffer: Buffer, options?: {
|
|
22
|
+
sheet?: string;
|
|
23
|
+
}): Promise<Row[]>;
|
|
19
24
|
export declare function iterateRowsFileBufferBatches(buffer: Buffer, format: RowsFileFormat, options?: {
|
|
20
25
|
sheet?: string;
|
|
21
26
|
batchSize?: number;
|
|
@@ -32,6 +32,21 @@ export async function parseRowsFileBuffer(buffer, format, options = {}) {
|
|
|
32
32
|
return await parseXlsxRows(buffer, options);
|
|
33
33
|
return parseRowsText(buffer.toString("utf8"), format);
|
|
34
34
|
}
|
|
35
|
+
/** Read raw workbook cells for consumers with stricter, domain-specific headers. */
|
|
36
|
+
export async function parseXlsxWorkbookBuffer(buffer, options = {}) {
|
|
37
|
+
try {
|
|
38
|
+
return await readXlsxFile(buffer, options.sheet ? { sheet: options.sheet } : {});
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
throw new OxygenError("invalid_rows", "XLSX sheet was not found.", {
|
|
42
|
+
details: {
|
|
43
|
+
sheet: options.sheet ?? null,
|
|
44
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
45
|
+
},
|
|
46
|
+
exitCode: 1,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
35
50
|
export async function* iterateRowsFileBufferBatches(buffer, format, options = {}) {
|
|
36
51
|
const batchSize = normalizeBatchSize(options.batchSize);
|
|
37
52
|
if (format === "csv") {
|
|
@@ -127,19 +142,7 @@ export function normalizeImportColumnKey(value) {
|
|
|
127
142
|
return toSnakeIdentifier(value, "column");
|
|
128
143
|
}
|
|
129
144
|
async function parseXlsxRows(buffer, options) {
|
|
130
|
-
|
|
131
|
-
try {
|
|
132
|
-
rows = await readXlsxFile(buffer, options.sheet ? { sheet: options.sheet } : {});
|
|
133
|
-
}
|
|
134
|
-
catch (error) {
|
|
135
|
-
throw new OxygenError("invalid_rows", "XLSX sheet was not found.", {
|
|
136
|
-
details: {
|
|
137
|
-
sheet: options.sheet ?? null,
|
|
138
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
139
|
-
},
|
|
140
|
-
exitCode: 1,
|
|
141
|
-
});
|
|
142
|
-
}
|
|
145
|
+
const rows = await parseXlsxWorkbookBuffer(buffer, options);
|
|
143
146
|
const [header, ...body] = rows;
|
|
144
147
|
if (!header || header.length === 0)
|
|
145
148
|
return [];
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export type MailboxImportMode = "identity" | "credential";
|
|
2
|
+
export type MailboxImportFileFormat = "json" | "jsonl" | "csv" | "xlsx";
|
|
3
|
+
export type MailboxImportValidationSource = "identity_file" | "credential_file" | "hypertide_file";
|
|
4
|
+
export type NormalizedMailboxImportRow = {
|
|
5
|
+
email_address: string;
|
|
6
|
+
provider: "google" | "microsoft";
|
|
7
|
+
workspace_external_id?: string;
|
|
8
|
+
infrastructure_platform?: "google_workspace" | "microsoft_365" | "microsoft_azure";
|
|
9
|
+
tenant_id?: string;
|
|
10
|
+
app_password?: string;
|
|
11
|
+
};
|
|
12
|
+
export type MailboxImportValidationSummary = {
|
|
13
|
+
valid: true;
|
|
14
|
+
source: MailboxImportValidationSource;
|
|
15
|
+
source_provider?: string;
|
|
16
|
+
rows: number;
|
|
17
|
+
providers: {
|
|
18
|
+
google: number;
|
|
19
|
+
microsoft: number;
|
|
20
|
+
};
|
|
21
|
+
infrastructure_platforms: Record<string, number>;
|
|
22
|
+
credential_rows: number;
|
|
23
|
+
identity_only_rows: number;
|
|
24
|
+
limits: {
|
|
25
|
+
max_rows: number;
|
|
26
|
+
max_file_bytes: number;
|
|
27
|
+
};
|
|
28
|
+
mutation: false;
|
|
29
|
+
network_request: false;
|
|
30
|
+
provider_call: false;
|
|
31
|
+
credits_used: 0;
|
|
32
|
+
next_action: string;
|
|
33
|
+
};
|
|
34
|
+
export declare const MAILBOX_IMPORT_FILE_MAX_BYTES: number;
|
|
35
|
+
export declare const MAILBOX_IMPORT_ROW_LIMIT = 500;
|
|
36
|
+
/**
|
|
37
|
+
* Infer the bounded mailbox-import format without node:path so browser and CLI
|
|
38
|
+
* preflight use the same extension contract.
|
|
39
|
+
*/
|
|
40
|
+
export declare function inferMailboxImportFileFormat(name: string): MailboxImportFileFormat;
|
|
41
|
+
/**
|
|
42
|
+
* Parse credential-sensitive text without ever echoing file contents. JSON may
|
|
43
|
+
* be either a bare array or the common `{ mailboxes: [...] }` export wrapper.
|
|
44
|
+
*/
|
|
45
|
+
export declare function parseMailboxImportText(text: string, format: Exclude<MailboxImportFileFormat, "xlsx">): unknown[];
|
|
46
|
+
/** Convert read-excel-file's browser rows to the same object rows as CSV. */
|
|
47
|
+
export declare function normalizeMailboxWorkbookRows(rows: readonly (readonly unknown[])[]): unknown[];
|
|
48
|
+
/**
|
|
49
|
+
* Whitelist mailbox fields before a local file crosses the network. The same
|
|
50
|
+
* function runs in the CLI and first-party web app, so accepted aliases and
|
|
51
|
+
* non-transferable-secret rejection cannot drift by surface.
|
|
52
|
+
*/
|
|
53
|
+
export declare function normalizeMailboxImportFile(value: unknown, mode: MailboxImportMode): NormalizedMailboxImportRow[];
|
|
54
|
+
export declare function summarizeMailboxImportValidation(mailboxes: readonly NormalizedMailboxImportRow[], input: {
|
|
55
|
+
source: MailboxImportValidationSource;
|
|
56
|
+
sourceProvider: string | null;
|
|
57
|
+
}): MailboxImportValidationSummary;
|
|
58
|
+
export declare function normalizeMailboxImportVendor(raw: string | null | undefined, from: string | null | undefined): string | null;
|
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
import { OxygenError } from "./cli-result.js";
|
|
2
|
+
export const MAILBOX_IMPORT_FILE_MAX_BYTES = 5 * 1024 * 1024;
|
|
3
|
+
export const MAILBOX_IMPORT_ROW_LIMIT = 500;
|
|
4
|
+
const MAILBOX_EMAIL_HEADERS = new Set([
|
|
5
|
+
"address",
|
|
6
|
+
"email",
|
|
7
|
+
"emailaddress",
|
|
8
|
+
"fromemail",
|
|
9
|
+
"mailbox",
|
|
10
|
+
"mailboxaddress",
|
|
11
|
+
"mailboxemail",
|
|
12
|
+
]);
|
|
13
|
+
const MAILBOX_PROVIDER_HEADERS = new Set([
|
|
14
|
+
"emailprovider",
|
|
15
|
+
"esp",
|
|
16
|
+
"mailboxprovider",
|
|
17
|
+
"platform",
|
|
18
|
+
"provider",
|
|
19
|
+
"serviceprovider",
|
|
20
|
+
"type",
|
|
21
|
+
]);
|
|
22
|
+
const MAILBOX_PASSWORD_HEADERS = new Set([
|
|
23
|
+
"apppassword",
|
|
24
|
+
"applicationpassword",
|
|
25
|
+
"googleapppassword",
|
|
26
|
+
"imappassword",
|
|
27
|
+
"mailboxpassword",
|
|
28
|
+
"password",
|
|
29
|
+
"smtppassword",
|
|
30
|
+
]);
|
|
31
|
+
const MAILBOX_WORKSPACE_HEADERS = new Set([
|
|
32
|
+
"externalaccountid",
|
|
33
|
+
"mailboxid",
|
|
34
|
+
"mailboxuid",
|
|
35
|
+
"sourceaccountid",
|
|
36
|
+
"sourcemailboxid",
|
|
37
|
+
"uid",
|
|
38
|
+
"workspaceexternalid",
|
|
39
|
+
]);
|
|
40
|
+
const MAILBOX_PLATFORM_HEADERS = new Set([
|
|
41
|
+
"infrastructure",
|
|
42
|
+
"infrastructureplatform",
|
|
43
|
+
"infraplatform",
|
|
44
|
+
]);
|
|
45
|
+
const MAILBOX_TENANT_HEADERS = new Set([
|
|
46
|
+
"azuretenantid",
|
|
47
|
+
"entratenantid",
|
|
48
|
+
"microsofttenantid",
|
|
49
|
+
"tenantid",
|
|
50
|
+
]);
|
|
51
|
+
const MAILBOX_NON_TRANSFERABLE_SECRET_HEADERS = new Set([
|
|
52
|
+
"accesstoken",
|
|
53
|
+
"applicationsecret",
|
|
54
|
+
"authenticatorsecret",
|
|
55
|
+
"authorization",
|
|
56
|
+
"authorizationcode",
|
|
57
|
+
"bearertoken",
|
|
58
|
+
"clientsecret",
|
|
59
|
+
"clientprivatekey",
|
|
60
|
+
"delegationkey",
|
|
61
|
+
"idtoken",
|
|
62
|
+
"mfacode",
|
|
63
|
+
"mfasecret",
|
|
64
|
+
"oauthaccesstoken",
|
|
65
|
+
"oauthcode",
|
|
66
|
+
"oauthrefreshtoken",
|
|
67
|
+
"oauthtoken",
|
|
68
|
+
"onetimepassword",
|
|
69
|
+
"otp",
|
|
70
|
+
"otpsecret",
|
|
71
|
+
"privatekey",
|
|
72
|
+
"refreshtoken",
|
|
73
|
+
"serviceaccountkey",
|
|
74
|
+
"serviceaccountjson",
|
|
75
|
+
"serviceaccountprivatekey",
|
|
76
|
+
"totp",
|
|
77
|
+
"totpsecret",
|
|
78
|
+
]);
|
|
79
|
+
/**
|
|
80
|
+
* Infer the bounded mailbox-import format without node:path so browser and CLI
|
|
81
|
+
* preflight use the same extension contract.
|
|
82
|
+
*/
|
|
83
|
+
export function inferMailboxImportFileFormat(name) {
|
|
84
|
+
const path = name.split(/[?#]/, 1)[0] ?? name;
|
|
85
|
+
const extension = path.slice(path.lastIndexOf(".")).toLowerCase();
|
|
86
|
+
if (extension === ".jsonl" || extension === ".ndjson")
|
|
87
|
+
return "jsonl";
|
|
88
|
+
if (extension === ".csv")
|
|
89
|
+
return "csv";
|
|
90
|
+
if (extension === ".xlsx" || extension === ".xlsm")
|
|
91
|
+
return "xlsx";
|
|
92
|
+
return "json";
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Parse credential-sensitive text without ever echoing file contents. JSON may
|
|
96
|
+
* be either a bare array or the common `{ mailboxes: [...] }` export wrapper.
|
|
97
|
+
*/
|
|
98
|
+
export function parseMailboxImportText(text, format) {
|
|
99
|
+
if (format === "csv")
|
|
100
|
+
return parseMailboxCsvRows(text);
|
|
101
|
+
if (format === "jsonl") {
|
|
102
|
+
return text
|
|
103
|
+
.split(/\r?\n/)
|
|
104
|
+
.map((line, index) => ({ line: line.trim(), lineNumber: index + 1 }))
|
|
105
|
+
.filter(({ line }) => Boolean(line))
|
|
106
|
+
.map(({ line, lineNumber }) => {
|
|
107
|
+
try {
|
|
108
|
+
return JSON.parse(line);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
throw new OxygenError("invalid_jsonl", `Mailbox import contains invalid JSON on line ${lineNumber}. File contents are omitted because they may contain credentials.`, { exitCode: 1 });
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
let parsed;
|
|
116
|
+
try {
|
|
117
|
+
parsed = JSON.parse(text);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
throw new OxygenError("invalid_json", "Mailbox import must contain valid JSON. File contents are omitted because they may contain credentials.", { exitCode: 1 });
|
|
121
|
+
}
|
|
122
|
+
if (Array.isArray(parsed))
|
|
123
|
+
return parsed;
|
|
124
|
+
if (parsed &&
|
|
125
|
+
typeof parsed === "object" &&
|
|
126
|
+
!Array.isArray(parsed) &&
|
|
127
|
+
Array.isArray(parsed.mailboxes)) {
|
|
128
|
+
return parsed.mailboxes;
|
|
129
|
+
}
|
|
130
|
+
throw new OxygenError("invalid_request", "Mailbox import JSON must contain an array or { mailboxes: [...] }.", { exitCode: 1 });
|
|
131
|
+
}
|
|
132
|
+
/** Convert read-excel-file's browser rows to the same object rows as CSV. */
|
|
133
|
+
export function normalizeMailboxWorkbookRows(rows) {
|
|
134
|
+
const [header, ...body] = rows;
|
|
135
|
+
if (!header || header.length === 0)
|
|
136
|
+
return [];
|
|
137
|
+
const labels = header
|
|
138
|
+
.map((cell, index) => ({
|
|
139
|
+
index,
|
|
140
|
+
label: cell === null || cell === undefined ? "" : String(cell).trim(),
|
|
141
|
+
}));
|
|
142
|
+
assertUniqueMailboxCsvHeaders(labels.map(({ label }) => label));
|
|
143
|
+
for (const [index, row] of body.entries()) {
|
|
144
|
+
if (row.length > header.length) {
|
|
145
|
+
throw new OxygenError("invalid_csv", `Workbook row ${index + 2} contains values beyond the named headers.`, { exitCode: 1 });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return body
|
|
149
|
+
.filter((row) => row.some((cell) => cell !== null && cell !== undefined && String(cell).trim()))
|
|
150
|
+
.map((row) => Object.fromEntries(labels.map(({ index, label }) => [
|
|
151
|
+
label,
|
|
152
|
+
normalizeMailboxWorkbookCell(row[index]),
|
|
153
|
+
])));
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Whitelist mailbox fields before a local file crosses the network. The same
|
|
157
|
+
* function runs in the CLI and first-party web app, so accepted aliases and
|
|
158
|
+
* non-transferable-secret rejection cannot drift by surface.
|
|
159
|
+
*/
|
|
160
|
+
export function normalizeMailboxImportFile(value, mode) {
|
|
161
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
162
|
+
throw new OxygenError("invalid_request", "The file must contain a non-empty mailboxes array.", { exitCode: 1 });
|
|
163
|
+
}
|
|
164
|
+
if (value.length > MAILBOX_IMPORT_ROW_LIMIT) {
|
|
165
|
+
throw new OxygenError("invalid_request", `Mailbox imports are limited to ${MAILBOX_IMPORT_ROW_LIMIT} rows per request.`, { exitCode: 1 });
|
|
166
|
+
}
|
|
167
|
+
const rows = value.map((entry, index) => {
|
|
168
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
169
|
+
throw new OxygenError("invalid_request", `mailboxes[${index}] must be an object.`, { exitCode: 1 });
|
|
170
|
+
}
|
|
171
|
+
return normalizeMailboxExportRow(entry, index, mode);
|
|
172
|
+
});
|
|
173
|
+
return dedupeMailboxImportRows(rows);
|
|
174
|
+
}
|
|
175
|
+
export function summarizeMailboxImportValidation(mailboxes, input) {
|
|
176
|
+
const providers = { google: 0, microsoft: 0 };
|
|
177
|
+
const infrastructurePlatforms = {};
|
|
178
|
+
let credentialRows = 0;
|
|
179
|
+
for (const mailbox of mailboxes) {
|
|
180
|
+
providers[mailbox.provider] += 1;
|
|
181
|
+
if (mailbox.infrastructure_platform) {
|
|
182
|
+
infrastructurePlatforms[mailbox.infrastructure_platform] =
|
|
183
|
+
(infrastructurePlatforms[mailbox.infrastructure_platform] ?? 0) + 1;
|
|
184
|
+
}
|
|
185
|
+
if (mailbox.app_password)
|
|
186
|
+
credentialRows += 1;
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
valid: true,
|
|
190
|
+
source: input.source,
|
|
191
|
+
...(input.sourceProvider ? { source_provider: input.sourceProvider } : {}),
|
|
192
|
+
rows: mailboxes.length,
|
|
193
|
+
providers,
|
|
194
|
+
infrastructure_platforms: infrastructurePlatforms,
|
|
195
|
+
credential_rows: credentialRows,
|
|
196
|
+
identity_only_rows: mailboxes.length - credentialRows,
|
|
197
|
+
limits: {
|
|
198
|
+
max_rows: MAILBOX_IMPORT_ROW_LIMIT,
|
|
199
|
+
max_file_bytes: MAILBOX_IMPORT_FILE_MAX_BYTES,
|
|
200
|
+
},
|
|
201
|
+
mutation: false,
|
|
202
|
+
network_request: false,
|
|
203
|
+
provider_call: false,
|
|
204
|
+
credits_used: 0,
|
|
205
|
+
next_action: "Submit this reviewed file to register the mailboxes.",
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
export function normalizeMailboxImportVendor(raw, from) {
|
|
209
|
+
if (from === "hypertide") {
|
|
210
|
+
if (raw && raw.trim().toLowerCase() !== "hypertide") {
|
|
211
|
+
throw new OxygenError("invalid_request", "Hypertide import provenance is fixed to hypertide.", { exitCode: 1 });
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
if (from === "credentials" && !raw) {
|
|
216
|
+
throw new OxygenError("invalid_request", "Name the provider that exported these credentials so each mailbox keeps its provenance.", { exitCode: 1 });
|
|
217
|
+
}
|
|
218
|
+
if (!raw)
|
|
219
|
+
return null;
|
|
220
|
+
const normalized = raw.trim().toLowerCase();
|
|
221
|
+
if (normalized.length < 1 ||
|
|
222
|
+
normalized.length > 64 ||
|
|
223
|
+
!/^[a-z0-9]+(?:[._-][a-z0-9]+)*$/.test(normalized)) {
|
|
224
|
+
throw new OxygenError("invalid_request", "Provider name must be 1-64 lowercase letters, digits, dots, underscores, or hyphens.", { exitCode: 1 });
|
|
225
|
+
}
|
|
226
|
+
return normalized;
|
|
227
|
+
}
|
|
228
|
+
function normalizeMailboxExportRow(row, index, mode) {
|
|
229
|
+
const byHeader = new Map();
|
|
230
|
+
for (const [header, value] of Object.entries(row)) {
|
|
231
|
+
const normalizedHeader = normalizeMailboxExportHeader(header);
|
|
232
|
+
const existing = byHeader.get(normalizedHeader);
|
|
233
|
+
if (existing !== undefined &&
|
|
234
|
+
String(existing).trim() !== String(value ?? "").trim()) {
|
|
235
|
+
throw new OxygenError("invalid_request", `mailboxes[${index}] contains conflicting columns that normalize to ${normalizedHeader}.`, { exitCode: 1 });
|
|
236
|
+
}
|
|
237
|
+
byHeader.set(normalizedHeader, value);
|
|
238
|
+
}
|
|
239
|
+
const email = readUniqueMailboxExportString(byHeader, MAILBOX_EMAIL_HEADERS, index, "email_address", (value) => value.trim().toLowerCase()) ?? readEmailShapedMailboxUsername(byHeader);
|
|
240
|
+
const providerValues = readMailboxExportStrings(byHeader, MAILBOX_PROVIDER_HEADERS);
|
|
241
|
+
const provider = normalizeMailboxProvider(providerValues, byHeader, index);
|
|
242
|
+
if (!email ||
|
|
243
|
+
email.length > 320 ||
|
|
244
|
+
!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
245
|
+
throw new OxygenError("invalid_request", `mailboxes[${index}].email_address must be a valid address of at most 320 characters.`, { exitCode: 1 });
|
|
246
|
+
}
|
|
247
|
+
if (!provider) {
|
|
248
|
+
throw new OxygenError("invalid_request", `mailboxes[${index}].provider could not be inferred. Oxygen imports Google Workspace and Microsoft 365/Entra identities; generic SMTP credentials are not transferable yet.`, { exitCode: 1 });
|
|
249
|
+
}
|
|
250
|
+
if (hasMailboxExportValue(byHeader, MAILBOX_NON_TRANSFERABLE_SECRET_HEADERS) ||
|
|
251
|
+
hasNonTransferableMailboxAuthLikeValue(byHeader)) {
|
|
252
|
+
throw new OxygenError("invalid_request", `mailboxes[${index}] contains OAuth, MFA/TOTP, client-secret, or delegation material. Those credentials are never transferable; import the mailbox identity and complete the provider consent flow.`, { exitCode: 1 });
|
|
253
|
+
}
|
|
254
|
+
const passwords = readMailboxExportStrings(byHeader, MAILBOX_PASSWORD_HEADERS);
|
|
255
|
+
const distinctPasswords = [...new Set(passwords)];
|
|
256
|
+
if (distinctPasswords.some((password) => password.length > 1024)) {
|
|
257
|
+
throw new OxygenError("invalid_request", `mailboxes[${index}].app_password must be at most 1024 characters.`, { exitCode: 1 });
|
|
258
|
+
}
|
|
259
|
+
if (mode === "identity" && distinctPasswords.length > 0) {
|
|
260
|
+
throw new OxygenError("invalid_request", "This identity-only import contains a password. Choose a secure credential import for a compatible Google app-password export.", { exitCode: 1 });
|
|
261
|
+
}
|
|
262
|
+
if (provider !== "microsoft" && distinctPasswords.length > 1) {
|
|
263
|
+
throw new OxygenError("invalid_request", `mailboxes[${index}] contains conflicting SMTP/IMAP/app-password values.`, { exitCode: 1 });
|
|
264
|
+
}
|
|
265
|
+
if (mode === "credential" &&
|
|
266
|
+
provider === "google" &&
|
|
267
|
+
distinctPasswords.length === 0) {
|
|
268
|
+
throw new OxygenError("invalid_request", `mailboxes[${index}] needs a Google app password for credential import. OAuth tokens and an ordinary account password are not portable.`, { exitCode: 1 });
|
|
269
|
+
}
|
|
270
|
+
const workspaceExternalId = readUniqueMailboxExportString(byHeader, MAILBOX_WORKSPACE_HEADERS, index, "workspace_external_id", (value) => value.trim());
|
|
271
|
+
if (workspaceExternalId && workspaceExternalId.length > 512) {
|
|
272
|
+
throw new OxygenError("invalid_request", `mailboxes[${index}].workspace_external_id must be at most 512 characters.`, { exitCode: 1 });
|
|
273
|
+
}
|
|
274
|
+
const tenantId = readUniqueMailboxExportString(byHeader, MAILBOX_TENANT_HEADERS, index, "tenant_id", (value) => value.trim().toLowerCase());
|
|
275
|
+
if (tenantId && provider !== "microsoft") {
|
|
276
|
+
throw new OxygenError("invalid_request", `mailboxes[${index}].tenant_id applies only to Microsoft mailboxes.`, { exitCode: 1 });
|
|
277
|
+
}
|
|
278
|
+
if (tenantId &&
|
|
279
|
+
!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(tenantId)) {
|
|
280
|
+
throw new OxygenError("invalid_request", `mailboxes[${index}].tenant_id must be a Microsoft Entra tenant GUID.`, { exitCode: 1 });
|
|
281
|
+
}
|
|
282
|
+
const infrastructurePlatformRaw = readUniqueMailboxExportString(byHeader, MAILBOX_PLATFORM_HEADERS, index, "infrastructure_platform", normalizeMailboxExportHeader) ??
|
|
283
|
+
(providerValues.some((value) => ["azure", "entra", "microsoftazure"].includes(normalizeMailboxExportHeader(value)))
|
|
284
|
+
? "microsoft_azure"
|
|
285
|
+
: null);
|
|
286
|
+
const infrastructurePlatform = normalizeMailboxInfrastructurePlatform(infrastructurePlatformRaw, provider, index);
|
|
287
|
+
return {
|
|
288
|
+
email_address: email.trim().toLowerCase(),
|
|
289
|
+
provider,
|
|
290
|
+
...(workspaceExternalId !== null
|
|
291
|
+
? { workspace_external_id: workspaceExternalId.trim() }
|
|
292
|
+
: {}),
|
|
293
|
+
...(infrastructurePlatformRaw
|
|
294
|
+
? { infrastructure_platform: infrastructurePlatform }
|
|
295
|
+
: {}),
|
|
296
|
+
...(tenantId ? { tenant_id: tenantId.trim().toLowerCase() } : {}),
|
|
297
|
+
...(mode === "credential" &&
|
|
298
|
+
provider === "google" &&
|
|
299
|
+
distinctPasswords[0] !== undefined
|
|
300
|
+
? { app_password: distinctPasswords[0] }
|
|
301
|
+
: {}),
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function normalizeMailboxExportHeader(value) {
|
|
305
|
+
return value
|
|
306
|
+
.replace(/^\uFEFF/, "")
|
|
307
|
+
.normalize("NFKC")
|
|
308
|
+
.trim()
|
|
309
|
+
.toLowerCase()
|
|
310
|
+
.replace(/[^a-z0-9]/g, "");
|
|
311
|
+
}
|
|
312
|
+
function readUniqueMailboxExportString(byHeader, headers, index, field, normalize) {
|
|
313
|
+
const values = readMailboxExportStrings(byHeader, headers);
|
|
314
|
+
if (values.length === 0)
|
|
315
|
+
return null;
|
|
316
|
+
const normalized = [...new Set(values.map(normalize))];
|
|
317
|
+
if (normalized.length > 1) {
|
|
318
|
+
throw new OxygenError("invalid_request", `mailboxes[${index}] contains conflicting ${field} aliases.`, { exitCode: 1 });
|
|
319
|
+
}
|
|
320
|
+
return normalized[0] ?? null;
|
|
321
|
+
}
|
|
322
|
+
function readMailboxExportStrings(byHeader, headers) {
|
|
323
|
+
const values = [];
|
|
324
|
+
for (const header of headers) {
|
|
325
|
+
const value = byHeader.get(header);
|
|
326
|
+
if (typeof value === "string" && value.length > 0)
|
|
327
|
+
values.push(value);
|
|
328
|
+
}
|
|
329
|
+
return values;
|
|
330
|
+
}
|
|
331
|
+
function hasMailboxExportValue(byHeader, headers) {
|
|
332
|
+
for (const header of headers) {
|
|
333
|
+
const value = byHeader.get(header);
|
|
334
|
+
if (value !== undefined && value !== null && String(value).trim())
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
function hasNonTransferableMailboxAuthLikeValue(byHeader) {
|
|
340
|
+
for (const [header, value] of byHeader) {
|
|
341
|
+
if (/(?:token|secret|privatekey|totp|passcode|authorization)/.test(header) &&
|
|
342
|
+
value !== undefined &&
|
|
343
|
+
value !== null &&
|
|
344
|
+
String(value).trim()) {
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
function readEmailShapedMailboxUsername(byHeader) {
|
|
351
|
+
for (const header of ["username", "imapusername", "smtpusername"]) {
|
|
352
|
+
const value = byHeader.get(header);
|
|
353
|
+
if (typeof value === "string" && value.includes("@")) {
|
|
354
|
+
return value.trim().toLowerCase();
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
function normalizeMailboxProvider(providerValues, byHeader, index) {
|
|
360
|
+
const explicit = providerValues.map((value) => {
|
|
361
|
+
const normalized = normalizeMailboxExportHeader(value);
|
|
362
|
+
if (["google", "googleworkspace", "gmail", "gsuite"].includes(normalized)) {
|
|
363
|
+
return "google";
|
|
364
|
+
}
|
|
365
|
+
if ([
|
|
366
|
+
"azure",
|
|
367
|
+
"entra",
|
|
368
|
+
"m365",
|
|
369
|
+
"microsoft",
|
|
370
|
+
"microsoft365",
|
|
371
|
+
"microsoftazure",
|
|
372
|
+
"ms365",
|
|
373
|
+
"o365",
|
|
374
|
+
"office365",
|
|
375
|
+
"outlook",
|
|
376
|
+
].includes(normalized)) {
|
|
377
|
+
return "microsoft";
|
|
378
|
+
}
|
|
379
|
+
throw new OxygenError("invalid_request", `mailboxes[${index}].provider is unsupported; expected Google/Gmail/Workspace or Microsoft/Outlook/Office 365/Entra.`, { exitCode: 1 });
|
|
380
|
+
});
|
|
381
|
+
const explicitKinds = [...new Set(explicit)];
|
|
382
|
+
if (explicitKinds.length > 1) {
|
|
383
|
+
throw new OxygenError("invalid_request", `mailboxes[${index}] contains conflicting provider aliases.`, { exitCode: 1 });
|
|
384
|
+
}
|
|
385
|
+
const hostValues = [
|
|
386
|
+
byHeader.get("imaphost"),
|
|
387
|
+
byHeader.get("imaphostname"),
|
|
388
|
+
byHeader.get("imapserver"),
|
|
389
|
+
byHeader.get("smtphost"),
|
|
390
|
+
byHeader.get("smtphostname"),
|
|
391
|
+
byHeader.get("smtpserver"),
|
|
392
|
+
].filter((value) => typeof value === "string" && Boolean(value.trim()));
|
|
393
|
+
const hostKinds = hostValues.map((value) => {
|
|
394
|
+
const hostname = normalizeMailboxHost(value);
|
|
395
|
+
if (hostname === "smtp.gmail.com" || hostname === "imap.gmail.com") {
|
|
396
|
+
return "google";
|
|
397
|
+
}
|
|
398
|
+
if (hostname === "smtp.office365.com" ||
|
|
399
|
+
hostname === "outlook.office365.com") {
|
|
400
|
+
return "microsoft";
|
|
401
|
+
}
|
|
402
|
+
throw new OxygenError("invalid_request", `mailboxes[${index}] contains a non-standard SMTP/IMAP host. Oxygen imports Google Workspace and Microsoft 365/Entra identities; generic SMTP credential transport is not supported.`, { exitCode: 1 });
|
|
403
|
+
});
|
|
404
|
+
const inferredKinds = [...new Set(hostKinds)];
|
|
405
|
+
if (inferredKinds.length > 1) {
|
|
406
|
+
throw new OxygenError("invalid_request", `mailboxes[${index}] contains conflicting Google and Microsoft mail hosts.`, { exitCode: 1 });
|
|
407
|
+
}
|
|
408
|
+
const explicitKind = explicitKinds[0];
|
|
409
|
+
const inferredKind = inferredKinds[0];
|
|
410
|
+
if (explicitKind && inferredKind && explicitKind !== inferredKind) {
|
|
411
|
+
throw new OxygenError("invalid_request", `mailboxes[${index}] has conflicting provider and SMTP/IMAP host evidence.`, { exitCode: 1 });
|
|
412
|
+
}
|
|
413
|
+
return explicitKind ?? inferredKind ?? null;
|
|
414
|
+
}
|
|
415
|
+
function normalizeMailboxHost(value) {
|
|
416
|
+
const withoutScheme = value
|
|
417
|
+
.trim()
|
|
418
|
+
.toLowerCase()
|
|
419
|
+
.replace(/^[a-z]+:\/\//, "");
|
|
420
|
+
return withoutScheme.split(/[/:]/, 1)[0] ?? "";
|
|
421
|
+
}
|
|
422
|
+
function normalizeMailboxInfrastructurePlatform(raw, provider, index) {
|
|
423
|
+
if (!raw)
|
|
424
|
+
return provider === "google" ? "google_workspace" : "microsoft_365";
|
|
425
|
+
const normalized = normalizeMailboxExportHeader(raw);
|
|
426
|
+
if (["google", "googleworkspace", "gmail", "gsuite", "workspace"].includes(normalized)) {
|
|
427
|
+
if (provider !== "google")
|
|
428
|
+
throw mailboxPlatformConflict(index, provider);
|
|
429
|
+
return "google_workspace";
|
|
430
|
+
}
|
|
431
|
+
if (["azure", "entra", "microsoftazure"].includes(normalized)) {
|
|
432
|
+
if (provider !== "microsoft")
|
|
433
|
+
throw mailboxPlatformConflict(index, provider);
|
|
434
|
+
return "microsoft_azure";
|
|
435
|
+
}
|
|
436
|
+
if ([
|
|
437
|
+
"m365",
|
|
438
|
+
"microsoft",
|
|
439
|
+
"microsoft365",
|
|
440
|
+
"ms365",
|
|
441
|
+
"o365",
|
|
442
|
+
"office365",
|
|
443
|
+
"outlook",
|
|
444
|
+
].includes(normalized)) {
|
|
445
|
+
if (provider !== "microsoft")
|
|
446
|
+
throw mailboxPlatformConflict(index, provider);
|
|
447
|
+
return "microsoft_365";
|
|
448
|
+
}
|
|
449
|
+
throw new OxygenError("invalid_request", `mailboxes[${index}].infrastructure_platform is unsupported; expected Google Workspace, Microsoft 365, Azure, or Entra.`, { exitCode: 1 });
|
|
450
|
+
}
|
|
451
|
+
function mailboxPlatformConflict(index, provider) {
|
|
452
|
+
return new OxygenError("invalid_request", `mailboxes[${index}].infrastructure_platform conflicts with provider=${provider}.`, { exitCode: 1 });
|
|
453
|
+
}
|
|
454
|
+
function dedupeMailboxImportRows(rows) {
|
|
455
|
+
const unique = new Map();
|
|
456
|
+
for (const row of rows) {
|
|
457
|
+
const address = row.email_address.toLowerCase();
|
|
458
|
+
const existing = unique.get(address);
|
|
459
|
+
if (!existing) {
|
|
460
|
+
unique.set(address, row);
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
if (JSON.stringify(existing) !== JSON.stringify(row)) {
|
|
464
|
+
throw new OxygenError("invalid_request", `Duplicate mailbox ${address} has conflicting provider, platform, tenant, external id, or credential fields.`, { exitCode: 1 });
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return [...unique.values()];
|
|
468
|
+
}
|
|
469
|
+
function parseMailboxCsvRows(text) {
|
|
470
|
+
const records = parseMailboxCsvRecords(text);
|
|
471
|
+
const [header, ...rows] = records;
|
|
472
|
+
if (!header || header.length === 0)
|
|
473
|
+
return [];
|
|
474
|
+
assertUniqueMailboxCsvHeaders(header);
|
|
475
|
+
for (const [index, row] of rows.entries()) {
|
|
476
|
+
if (row.length > header.length) {
|
|
477
|
+
throw new OxygenError("invalid_csv", `CSV row ${index + 2} contains more fields than the header.`, { exitCode: 1 });
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return rows
|
|
481
|
+
.filter((row) => row.some((cell) => cell.trim()))
|
|
482
|
+
.map((row) => Object.fromEntries(header.map((key, index) => [
|
|
483
|
+
key,
|
|
484
|
+
row[index] === undefined || row[index] === "" ? null : row[index],
|
|
485
|
+
])));
|
|
486
|
+
}
|
|
487
|
+
function parseMailboxCsvRecords(text) {
|
|
488
|
+
const records = [];
|
|
489
|
+
let record = [];
|
|
490
|
+
let field = "";
|
|
491
|
+
let inQuotes = false;
|
|
492
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
493
|
+
const char = text[index];
|
|
494
|
+
const next = text[index + 1];
|
|
495
|
+
if (inQuotes) {
|
|
496
|
+
if (char === '"' && next === '"') {
|
|
497
|
+
field += '"';
|
|
498
|
+
index += 1;
|
|
499
|
+
}
|
|
500
|
+
else if (char === '"') {
|
|
501
|
+
inQuotes = false;
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
field += char;
|
|
505
|
+
}
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
if (char === '"')
|
|
509
|
+
inQuotes = true;
|
|
510
|
+
else if (char === ",") {
|
|
511
|
+
record.push(field);
|
|
512
|
+
field = "";
|
|
513
|
+
}
|
|
514
|
+
else if (char === "\n") {
|
|
515
|
+
record.push(field);
|
|
516
|
+
records.push(record);
|
|
517
|
+
record = [];
|
|
518
|
+
field = "";
|
|
519
|
+
}
|
|
520
|
+
else if (char !== "\r") {
|
|
521
|
+
field += char;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
if (inQuotes) {
|
|
525
|
+
throw new OxygenError("invalid_csv", "CSV contains an unterminated quoted field.", {
|
|
526
|
+
exitCode: 1,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
if (field || record.length > 0) {
|
|
530
|
+
record.push(field);
|
|
531
|
+
records.push(record);
|
|
532
|
+
}
|
|
533
|
+
return records;
|
|
534
|
+
}
|
|
535
|
+
function assertUniqueMailboxCsvHeaders(header) {
|
|
536
|
+
const seen = new Set();
|
|
537
|
+
for (const value of header) {
|
|
538
|
+
const normalized = value.normalize("NFKC").trim().toLowerCase();
|
|
539
|
+
if (!normalized || seen.has(normalized)) {
|
|
540
|
+
throw new OxygenError("invalid_csv", "CSV/XLSX headers must be non-empty and unique (case-insensitive).", { exitCode: 1 });
|
|
541
|
+
}
|
|
542
|
+
seen.add(normalized);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
function normalizeMailboxWorkbookCell(value) {
|
|
546
|
+
if (value === undefined || value === null || value === "")
|
|
547
|
+
return null;
|
|
548
|
+
if (typeof value === "string" ||
|
|
549
|
+
typeof value === "number" ||
|
|
550
|
+
typeof value === "boolean") {
|
|
551
|
+
return value;
|
|
552
|
+
}
|
|
553
|
+
if (value instanceof Date)
|
|
554
|
+
return value.toISOString();
|
|
555
|
+
return String(value);
|
|
556
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const OXYGEN_VERSION = "1.
|
|
1
|
+
export const OXYGEN_VERSION = "1.681.2";
|
|
2
2
|
// The GLOBAL CLI compatibility floor: the oldest CLI allowed to call any
|
|
3
3
|
// operational route. Raising it hard-rejects every older CLI from the entire
|
|
4
4
|
// product, so it obeys one law, enforced by scripts/ci/cli-min-version-gate.mjs:
|
|
@@ -21,6 +21,11 @@
|
|
|
21
21
|
"import": "./dist/file-import.js",
|
|
22
22
|
"default": "./dist/file-import.js"
|
|
23
23
|
},
|
|
24
|
+
"./mailbox-import": {
|
|
25
|
+
"types": "./dist/mailbox-import.d.ts",
|
|
26
|
+
"import": "./dist/mailbox-import.js",
|
|
27
|
+
"default": "./dist/mailbox-import.js"
|
|
28
|
+
},
|
|
24
29
|
"./linkedin-url": {
|
|
25
30
|
"types": "./dist/linkedin-url.d.ts",
|
|
26
31
|
"import": "./dist/linkedin-url.js",
|