diff --git a/README.md b/README.md index 523fb4c..085b1a7 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ 一个用于批量跑通 ChatGPT OAuth 注册/登录流程的 Chrome 扩展。 -当前版本基于侧边栏控制,支持单步执行、整套自动执行、停止当前流程、保存常用配置,以及通过 DuckDuckGo / QQ / 163 / 163 VIP / 126 / Inbucket / Hotmail 协助获取验证码。 +当前版本基于侧边栏控制,支持单步执行、整套自动执行、停止当前流程、保存常用配置,以及通过 DuckDuckGo / QQ / 163 / 163 VIP / 126 / Inbucket / Hotmail / Cloud Mail 协助获取验证码。 ## 插件效果 @@ -40,6 +40,7 @@ - 自动获取注册验证码与登录验证码 - 支持 `Hotmail`:继续使用 `邮箱 + 客户端 ID + 刷新令牌(refresh token)`,并可在远程服务与本地助手两种模式间切换 - 支持 `2925`:新增多账号池、自动登录登出、Step 4 / Step 8 命中“子邮箱已达上限邮箱”后的 24 小时禁用与自动切号 +- 支持 `Cloud Mail`:可通过 skymail.ink API 生成自定义域邮箱,也可作为转发收件通道轮询验证码 - 支持 `QQ Mail`、`163 Mail`、`163 VIP Mail`、`126 Mail`、`Inbucket mailbox` - 支持从 DuckDuckGo Email Protection 自动生成新的 `@duck.com` 地址 - 支持基于 Cloudflare 自定义域名自动生成随机邮箱前缀 @@ -208,10 +209,11 @@ Step 1 和 Step 10 都依赖这个地址。 ### `Mail` -支持七种验证码来源: +支持八种验证码来源: - `Hotmail` - `2925` +- `Cloud Mail` - `163 Mail` - `163 VIP Mail` - `126 Mail` @@ -222,6 +224,7 @@ Step 1 和 Step 10 都依赖这个地址。 - `Hotmail` 通过侧边栏里的 Hotmail 账号池选择账号,可切换为远程服务模式或本地助手模式 - `2925` 通过侧边栏里的 2925 账号池选择账号,并在 Step 4 / Step 8 前自动校验网页邮箱登录态 +- `Cloud Mail` 通过侧边栏配置 API 地址、管理员账号、接收邮箱或生成域名,可直接生成邮箱或轮询转发收件箱 - `QQ`、`163`、`163 VIP`、`126` 用于直接轮询网页邮箱 - `Inbucket` 通过你在侧边栏里配置的 host 访问 `mailbox` 页面:`https:///m//` @@ -521,7 +524,7 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。 1. Step 1 打开 `https://chatgpt.com/` 2. 根据 `Mail` 选择邮箱来源 3. 如果 `Mail = Hotmail`,会从账号池自动分配一个可用账号 -4. 如果 `Mail = 自定义邮箱` 且配置了 `自定义号池`,会按号池顺序分配当前轮邮箱;否则如果不是 Hotmail,则按当前“邮箱生成”配置尝试自动获取或分配邮箱(Duck / Cloudflare / iCloud / 自定义邮箱池等) +4. 如果 `Mail = 自定义邮箱` 且配置了 `自定义号池`,会按号池顺序分配当前轮邮箱;否则如果不是 Hotmail,则按当前“邮箱生成”配置尝试自动获取或分配邮箱(Duck / Cloudflare / Cloud Mail / iCloud / 自定义邮箱池等) 5. Step 2 点击注册、填写邮箱,并按真实落地页进入密码页或直接进入邮箱验证码页 6. 如果自动获取失败,暂停并等待你在侧边栏填写邮箱后点击 `Continue` 7. 继续执行 Step 3 ~ Step 10 diff --git a/background.js b/background.js index 9c5407f..05e6e94 100644 --- a/background.js +++ b/background.js @@ -48,6 +48,8 @@ importScripts( 'microsoft-email.js', 'luckmail-utils.js', 'cloudflare-temp-email-utils.js', + 'cloudmail-utils.js', + 'background/cloudmail-provider.js', 'icloud-utils.js', 'mail-provider-utils.js', 'content/activation-utils.js' @@ -168,6 +170,17 @@ const { normalizeCloudflareTempEmailDomains, normalizeCloudflareTempEmailMailApiMessages, } = self.CloudflareTempEmailUtils; +const { + DEFAULT_MAIL_PAGE_SIZE: CLOUD_MAIL_DEFAULT_PAGE_SIZE, + buildCloudMailHeaders, + getCloudMailTokenFromResponse, + joinCloudMailUrl, + normalizeCloudMailAddress, + normalizeCloudMailBaseUrl, + normalizeCloudMailDomain, + normalizeCloudMailDomains, + normalizeCloudMailMailApiMessages, +} = self.CloudMailUtils; const { findIcloudAliasByEmail, getConfiguredIcloudHostPreference, @@ -224,6 +237,8 @@ const HOTMAIL_PROVIDER = 'hotmail-api'; const LUCKMAIL_PROVIDER = 'luckmail-api'; const CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email'; const CLOUDFLARE_TEMP_EMAIL_GENERATOR = 'cloudflare-temp-email'; +const CLOUD_MAIL_PROVIDER = 'cloudmail'; +const CLOUD_MAIL_GENERATOR = 'cloudmail'; const CUSTOM_EMAIL_POOL_GENERATOR = 'custom-pool'; const HOTMAIL_MAILBOXES = ['INBOX', 'Junk']; const STOP_ERROR_MESSAGE = '流程已被用户停止。'; @@ -378,8 +393,18 @@ const FIVE_SIM_COUNTRY_ID = 'vietnam'; const FIVE_SIM_COUNTRY_LABEL = '越南 (Vietnam)'; const FIVE_SIM_SUPPORTED_COUNTRY_IDS = ['indonesia', 'thailand', 'vietnam']; const FIVE_SIM_SUPPORTED_COUNTRY_ID_SET = new Set(FIVE_SIM_SUPPORTED_COUNTRY_IDS); -const HERO_SMS_SUPPORTED_COUNTRY_IDS = [6, 52, 10]; +const HERO_SMS_SUPPORTED_COUNTRY_IDS = [6, 52, 187, 16, 151, 43, 73, 10]; const HERO_SMS_SUPPORTED_COUNTRY_ID_SET = new Set(HERO_SMS_SUPPORTED_COUNTRY_IDS.map(String)); +const HERO_SMS_COUNTRY_BY_PHONE_PREFIX = Object.freeze([ + { prefix: '84', id: 10, label: 'Vietnam' }, + { prefix: '66', id: 52, label: 'Thailand' }, + { prefix: '62', id: 6, label: 'Indonesia' }, + { prefix: '44', id: 16, label: 'United Kingdom' }, + { prefix: '81', id: 151, label: 'Japan' }, + { prefix: '49', id: 43, label: 'Germany' }, + { prefix: '33', id: 73, label: 'France' }, + { prefix: '1', id: 187, label: 'USA' }, +]); const FIVE_SIM_OPERATOR = DEFAULT_FIVE_SIM_OPERATOR; const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal'; const PLUS_PAYMENT_METHOD_GOPAY = 'gopay'; @@ -698,6 +723,13 @@ const PERSISTED_SETTING_DEFAULTS = { cloudflareTempEmailUseRandomSubdomain: false, cloudflareTempEmailDomain: '', cloudflareTempEmailDomains: [], + cloudMailBaseUrl: '', + cloudMailAdminEmail: '', + cloudMailAdminPassword: '', + cloudMailToken: '', + cloudMailReceiveMailbox: '', + cloudMailDomain: '', + cloudMailDomains: [], hotmailAccounts: [], mail2925Accounts: [], paypalAccounts: [], @@ -1690,6 +1722,7 @@ function normalizeEmailGenerator(value = '') { } if (normalized === 'cloudflare') return 'cloudflare'; if (normalized === CLOUDFLARE_TEMP_EMAIL_GENERATOR) return CLOUDFLARE_TEMP_EMAIL_GENERATOR; + if (normalized === 'cloudmail') return 'cloudmail'; return 'duck'; } @@ -1923,6 +1956,7 @@ function normalizeMailProvider(value = '') { case HOTMAIL_PROVIDER: case LUCKMAIL_PROVIDER: case CLOUDFLARE_TEMP_EMAIL_PROVIDER: + case CLOUD_MAIL_PROVIDER: case '163': case '163-vip': case '126': @@ -2118,6 +2152,34 @@ function resolveCloudflareTempEmailPollTargetEmail(state = {}, pollPayload = {}, return normalizeCloudflareTempEmailReceiveMailbox(state.email); } +const cloudMailProvider = self.MultiPageBackgroundCloudMailProvider.createCloudMailProvider({ + addLog, + buildCloudMailHeaders, + CLOUD_MAIL_DEFAULT_PAGE_SIZE, + CLOUD_MAIL_GENERATOR, + CLOUD_MAIL_PROVIDER, + getCloudMailTokenFromResponse, + getState, + joinCloudMailUrl, + normalizeCloudMailAddress, + normalizeCloudMailBaseUrl, + normalizeCloudMailDomain, + normalizeCloudMailDomains, + normalizeCloudMailMailApiMessages, + pickVerificationMessageWithTimeFallback, + setEmailState, + setPersistentSettings, + sleepWithStop, + throwIfStopped, +}); +const { + getCloudMailConfig, + normalizeCloudMailReceiveMailbox, + fetchCloudMailAddress, + pollCloudMailVerificationCode, + resolveCloudMailPollTargetEmail, +} = cloudMailProvider; + function normalizeSub2ApiGroupNames(value = '') { const source = Array.isArray(value) ? value @@ -2454,6 +2516,19 @@ function normalizePersistentSettingValue(key, value) { return normalizeCloudflareTempEmailDomain(value); case 'cloudflareTempEmailDomains': return normalizeCloudflareTempEmailDomains(value); + case 'cloudMailBaseUrl': + return normalizeCloudMailBaseUrl(value); + case 'cloudMailAdminEmail': + return String(value || '').trim(); + case 'cloudMailAdminPassword': + case 'cloudMailToken': + return String(value || ''); + case 'cloudMailReceiveMailbox': + return normalizeCloudMailReceiveMailbox(value); + case 'cloudMailDomain': + return normalizeCloudMailDomain(value); + case 'cloudMailDomains': + return normalizeCloudMailDomains(value); case 'hotmailAccounts': return normalizeHotmailAccounts(value); case 'mail2925Accounts': @@ -2563,6 +2638,13 @@ function buildPersistentSettingsPayload(input = {}, options = {}) { } payload.cloudflareTempEmailDomains = domains; } + if (payload.cloudMailDomains) { + const domains = normalizeCloudMailDomains(payload.cloudMailDomains); + if (payload.cloudMailDomain && !domains.includes(payload.cloudMailDomain)) { + domains.unshift(payload.cloudMailDomain); + } + payload.cloudMailDomains = domains; + } if ( Object.prototype.hasOwnProperty.call(payload, 'sub2apiGroupName') || Object.prototype.hasOwnProperty.call(payload, 'sub2apiGroupNames') @@ -6708,19 +6790,117 @@ async function clearFreeReusablePhoneActivation() { return { ok: true, freeReusablePhoneActivation: null }; } +function inferHeroSmsCountryFromPhoneNumber(phoneNumber = '') { + const digits = String(phoneNumber || '').replace(/\D+/g, ''); + if (!digits) { + return null; + } + const match = HERO_SMS_COUNTRY_BY_PHONE_PREFIX.find((entry) => digits.startsWith(entry.prefix)); + if (!match) { + return null; + } + return { + id: Math.max(1, Math.floor(Number(match.id) || 0)), + label: String(match.label || '').trim() || `Country #${match.id}`, + }; +} + +function normalizePhoneDigits(value = '') { + return String(value || '').replace(/\D+/g, ''); +} + +function phoneNumbersMatch(left = '', right = '') { + const leftDigits = normalizePhoneDigits(left); + const rightDigits = normalizePhoneDigits(right); + return Boolean(leftDigits && rightDigits && leftDigits === rightDigits); +} + +function normalizeLocalHeroSmsActivation(record) { + if (!record || typeof record !== 'object' || Array.isArray(record)) { + return null; + } + const activationId = String(record.activationId ?? record.id ?? record.activation ?? '').trim(); + const phoneNumber = String(record.phoneNumber ?? record.number ?? record.phone ?? '').trim(); + if (!activationId || !phoneNumber) { + return null; + } + const rawProvider = String(record.provider ?? record.smsProvider ?? '').trim(); + const provider = rawProvider ? normalizePhoneSmsProvider(rawProvider) : PHONE_SMS_PROVIDER_HERO; + if (provider !== PHONE_SMS_PROVIDER_HERO) { + return null; + } + const countryId = Math.max( + 0, + Math.floor(Number(record.countryId ?? record.country ?? record.countryCode) || 0) + ); + const countryLabel = String(record.countryLabel || record.label || '').trim(); + const serviceCode = String(record.serviceCode || record.service || HERO_SMS_SERVICE_CODE).trim() || HERO_SMS_SERVICE_CODE; + return { + ...record, + provider: PHONE_SMS_PROVIDER_HERO, + activationId, + phoneNumber, + serviceCode, + ...(countryId > 0 ? { countryId } : {}), + ...(countryLabel ? { countryLabel } : {}), + }; +} + +function findLocalHeroSmsActivationForPhone(state = {}, phoneNumber = '') { + const candidates = [ + state.currentPhoneActivation, + state.reusablePhoneActivation, + state.pendingPhoneActivationConfirmation, + state.signupPhoneActivation, + state.signupPhoneCompletedActivation, + state.phonePreferredActivation, + state.freeReusablePhoneActivation, + ]; + if (Array.isArray(state.phoneReusableActivationPool)) { + candidates.push(...state.phoneReusableActivationPool); + } + for (const candidate of candidates) { + const normalized = normalizeLocalHeroSmsActivation(candidate); + if (normalized && phoneNumbersMatch(normalized.phoneNumber, phoneNumber)) { + return normalized; + } + } + return null; +} + async function setFreeReusablePhoneActivation(record = {}) { const phoneNumber = String(record.phoneNumber || record.number || record.phone || '').trim(); if (!phoneNumber) { throw new Error('请先填写白嫖复用手机号。'); } const state = await getState(); - const activationId = String(record.activationId || record.id || record.activation || '').trim(); - const countryId = Math.max(1, Math.floor(Number(record.countryId) || Number(state.heroSmsCountryId) || HERO_SMS_COUNTRY_ID)); + const localActivation = findLocalHeroSmsActivationForPhone(state, phoneNumber); + const activationId = String( + record.activationId + || record.id + || record.activation + || localActivation?.activationId + || '' + ).trim(); + const inferredCountry = inferHeroSmsCountryFromPhoneNumber(phoneNumber); + const hasExplicitCountry = Number.isFinite(Number(record.countryId)) && Number(record.countryId) > 0; + const countryId = Math.max( + 1, + Math.floor( + Number(record.countryId) + || Number(localActivation?.countryId) + || Number(inferredCountry?.id) + || Number(state.heroSmsCountryId) + || HERO_SMS_COUNTRY_ID + ) + ); const stateCountryLabel = Math.floor(Number(state.heroSmsCountryId) || 0) === countryId ? String(state.heroSmsCountryLabel || '').trim() : ''; const countryLabel = String( record.countryLabel + || (Number(localActivation?.countryId) === countryId ? localActivation?.countryLabel : '') + || (!hasExplicitCountry && inferredCountry?.id === countryId ? inferredCountry.label : '') || stateCountryLabel || (countryId === HERO_SMS_COUNTRY_ID ? HERO_SMS_COUNTRY_LABEL : `Country #${countryId}`) ).trim(); @@ -6728,7 +6908,7 @@ async function setFreeReusablePhoneActivation(record = {}) { ...(activationId ? { activationId } : {}), phoneNumber, provider: PHONE_SMS_PROVIDER_HERO, - serviceCode: HERO_SMS_SERVICE_CODE, + serviceCode: String(record.serviceCode || localActivation?.serviceCode || HERO_SMS_SERVICE_CODE).trim() || HERO_SMS_SERVICE_CODE, countryId, ...(countryLabel ? { countryLabel } : {}), successfulUses: Math.max(0, Math.floor(Number(record.successfulUses) || 0)), @@ -7231,6 +7411,7 @@ function getSourceLabel(source) { 'hotmail-api': 'Hotmail(API对接/本地助手)', 'luckmail-api': 'LuckMail(API 购邮)', 'cloudflare-temp-email': 'Cloudflare Temp Email', + 'cloudmail': 'Cloud Mail', 'plus-checkout': 'Plus Checkout', 'paypal-flow': 'PayPal 授权页', }; @@ -7495,12 +7676,20 @@ async function restartSignupPhonePasswordMismatchAttemptFromStep(step, restartCo } = getSignupPhonePasswordMismatchRestartPayload(preservedState); const emailSuffix = preservedEmail ? `当前邮箱:${preservedEmail};` : ''; const phoneSuffix = activeSignupPhoneNumber ? `当前手机号:${activeSignupPhoneNumber};` : ''; + const errorMessage = getErrorMessage(error); + const reasonLabel = /PHONE_RESEND_BANNED_NUMBER::|无法向此(?:电话|手机)号码发送短信|无法发送短信到此(?:电话|手机)号码|unable\s+to\s+send\s+(?:an?\s+)?(?:sms|text(?:\s+message)?)\s+to\s+(?:this|that)\s+(?:phone\s+)?number/i + .test(errorMessage) + ? '当前注册手机号无法接收短信' + : (/与此(?:电话|手机)号码相关联的帐户已存在|account\s+associated\s+with\s+this\s+phone\s+number\s+already\s+exists/i + .test(errorMessage) + ? '注册手机号异常' + : '手机号/密码不匹配'); await addLog( - `步骤 ${step}:检测到手机号/密码不匹配,准备丢弃当前注册手机号并回到步骤 1 重新开始(第 ${restartCount} 次重开)。${phoneSuffix}${emailSuffix}原因:${getErrorMessage(error)}`, + `步骤 ${step}:检测到${reasonLabel},准备丢弃当前注册手机号并回到步骤 1 重新开始(第 ${restartCount} 次重开)。${phoneSuffix}${emailSuffix}原因:${errorMessage}`, 'warn' ); await invalidateDownstreamAfterStepRestart(1, { - logLabel: `步骤 ${step} 检测到手机号/密码不匹配后准备回到步骤 1 重新获取手机号重试(第 ${restartCount} 次重开)`, + logLabel: `步骤 ${step} 检测到${reasonLabel}后准备回到步骤 1 重新获取手机号重试(第 ${restartCount} 次重开)`, }); if (shouldClearSignupPhoneRuntime) { await addLog(`步骤 ${step}:已清空本轮注册手机号与接码订单,下一次重开将重新获取号码。`, 'warn'); @@ -9239,6 +9428,7 @@ function getEmailGeneratorLabel(generator) { } if (generator === 'cloudflare') return 'Cloudflare 邮箱'; if (generator === CLOUDFLARE_TEMP_EMAIL_GENERATOR) return 'Cloudflare Temp Email'; + if (generator === CLOUD_MAIL_GENERATOR) return 'Cloud Mail'; return 'Duck 邮箱'; } const mail2925SessionManager = self.MultiPageBackgroundMail2925Session?.createMail2925SessionManager({ @@ -9412,6 +9602,11 @@ async function fetchDuckEmail(options = {}) { } async function fetchGeneratedEmail(state, options = {}) { + const currentState = state || await getState(); + const generator = normalizeEmailGenerator(options.generator ?? currentState.emailGenerator); + if (generator === CLOUD_MAIL_GENERATOR) { + return fetchCloudMailAddress(currentState, options); + } return generatedEmailHelpers.fetchGeneratedEmail(state, options); } @@ -10317,8 +10512,9 @@ async function runAutoSequenceFromStep(startStep, context = {}) { throw err; } step4RestartCount += 1; - const isPhoneResendBanned = typeof isPhoneResendBannedNumberError === 'function' - && isPhoneResendBannedNumberError(err); + const isPhoneResendBanned = typeof phoneVerificationHelpers !== 'undefined' + && typeof phoneVerificationHelpers?.isPhoneResendBannedNumberError === 'function' + && phoneVerificationHelpers.isPhoneResendBannedNumberError(err); if (isSignupPhonePasswordMismatchFailure(err) || isPhoneResendBanned) { await restartSignupPhonePasswordMismatchAttemptFromStep(4, step4RestartCount, err); } else { @@ -10554,6 +10750,7 @@ const verificationFlowHelpers = self.MultiPageBackgroundVerificationFlow?.create chrome, closeConflictingTabsForSource, CLOUDFLARE_TEMP_EMAIL_PROVIDER, + CLOUD_MAIL_PROVIDER, completeStepFromBackground, confirmCustomVerificationStepBypassRequest: (step) => chrome.runtime.sendMessage({ type: 'REQUEST_CUSTOM_VERIFICATION_BYPASS_CONFIRMATION', @@ -10572,6 +10769,7 @@ const verificationFlowHelpers = self.MultiPageBackgroundVerificationFlow?.create MAIL_2925_VERIFICATION_INTERVAL_MS, MAIL_2925_VERIFICATION_MAX_ATTEMPTS, pollCloudflareTempEmailVerificationCode, + pollCloudMailVerificationCode, pollHotmailVerificationCode, pollLuckmailVerificationCode, sendToContentScript, @@ -10677,6 +10875,7 @@ const step4Executor = self.MultiPageBackgroundStep4?.createStep4Executor({ isTabAlive, LUCKMAIL_PROVIDER, CLOUDFLARE_TEMP_EMAIL_PROVIDER, + CLOUD_MAIL_PROVIDER, resolveVerificationStep: verificationFlowHelpers.resolveVerificationStep, reuseOrCreateTab, sendToContentScript, @@ -10724,6 +10923,7 @@ const step8Executor = self.MultiPageBackgroundStep8?.createStep8Executor({ addLog, chrome, CLOUDFLARE_TEMP_EMAIL_PROVIDER, + CLOUD_MAIL_PROVIDER, completeStepFromBackground, confirmCustomVerificationStepBypass: verificationFlowHelpers.confirmCustomVerificationStepBypass, ensureMail2925MailboxSession, @@ -11141,6 +11341,9 @@ function getMailConfig(state) { if (provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) { return { provider: CLOUDFLARE_TEMP_EMAIL_PROVIDER, label: 'Cloudflare Temp Email' }; } + if (provider === 'cloudmail') { + return { provider: 'cloudmail', label: 'Cloud Mail' }; + } if (provider === '163') { return { source: 'mail-163', url: 'https://mail.163.com/js6/main.jsp?df=mail163_letter#module=mbox.ListModule%7C%7B%22fid%22%3A1%2C%22order%22%3A%22date%22%2C%22desc%22%3Atrue%7D', label: '163 邮箱' }; } diff --git a/background/cloudmail-provider.js b/background/cloudmail-provider.js new file mode 100644 index 0000000..9c8f01d --- /dev/null +++ b/background/cloudmail-provider.js @@ -0,0 +1,324 @@ +(function cloudMailProviderModule(root, factory) { + root.MultiPageBackgroundCloudMailProvider = factory(); +})(typeof self !== 'undefined' ? self : globalThis, function createCloudMailProviderModule() { + function createCloudMailProvider(deps = {}) { + const { + addLog = async () => {}, + buildCloudMailHeaders, + CLOUD_MAIL_DEFAULT_PAGE_SIZE = 20, + CLOUD_MAIL_GENERATOR = 'cloudmail', + CLOUD_MAIL_PROVIDER = 'cloudmail', + fetchImpl = typeof fetch === 'function' ? fetch.bind(globalThis) : null, + getCloudMailTokenFromResponse, + getState = async () => ({}), + joinCloudMailUrl, + normalizeCloudMailAddress, + normalizeCloudMailBaseUrl, + normalizeCloudMailDomain, + normalizeCloudMailDomains, + normalizeCloudMailMailApiMessages, + pickVerificationMessageWithTimeFallback, + setEmailState = async () => {}, + setPersistentSettings = async () => {}, + sleepWithStop = async () => {}, + throwIfStopped = () => {}, + } = deps; + + function getCloudMailConfig(state = {}) { + return { + baseUrl: normalizeCloudMailBaseUrl(state.cloudMailBaseUrl), + adminEmail: String(state.cloudMailAdminEmail || '').trim(), + adminPassword: String(state.cloudMailAdminPassword || ''), + token: String(state.cloudMailToken || '').trim(), + receiveMailbox: normalizeCloudMailReceiveMailbox(state.cloudMailReceiveMailbox), + domain: normalizeCloudMailDomain(state.cloudMailDomain), + domains: normalizeCloudMailDomains(state.cloudMailDomains), + }; + } + + function normalizeCloudMailReceiveMailbox(value = '') { + const normalized = normalizeCloudMailAddress(value); + if (!normalized) return ''; + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalized) ? normalized : ''; + } + + function resolveCloudMailPollTargetEmail(state = {}, pollPayload = {}, config = getCloudMailConfig(state)) { + const configuredReceiveMailbox = normalizeCloudMailReceiveMailbox(config.receiveMailbox); + const mailProvider = String(state?.mailProvider || '').trim().toLowerCase(); + const emailGenerator = String(state?.emailGenerator || '').trim().toLowerCase(); + const shouldPreferConfiguredReceiveMailbox = mailProvider === CLOUD_MAIL_PROVIDER + && emailGenerator !== CLOUD_MAIL_GENERATOR; + if (shouldPreferConfiguredReceiveMailbox && configuredReceiveMailbox) { + return configuredReceiveMailbox; + } + + const requestedTarget = normalizeCloudMailReceiveMailbox(pollPayload.targetEmail); + if (requestedTarget) { + return requestedTarget; + } + + return normalizeCloudMailReceiveMailbox(state.email); + } + + function ensureCloudMailConfig(state, options = {}) { + const { requireToken = false, requireCredentials = false, requireDomain = false } = options; + const config = getCloudMailConfig(state); + if (!config.baseUrl) { + throw new Error('Cloud Mail 服务地址为空或格式无效。'); + } + if (requireCredentials && (!config.adminEmail || !config.adminPassword)) { + throw new Error('Cloud Mail 缺少管理员邮箱或密码。'); + } + if (requireToken && !config.token) { + throw new Error('Cloud Mail 尚未获取到身份令牌,请先生成 Token。'); + } + if (requireDomain && !config.domain) { + throw new Error('Cloud Mail 域名为空或格式无效。'); + } + return config; + } + + async function requestCloudMailJson(config, path, options = {}) { + if (!fetchImpl) { + throw new Error('Cloud Mail 当前运行环境不支持 fetch。'); + } + const { + method = 'POST', + payload, + timeoutMs = 20000, + requireToken = true, + } = options; + const url = joinCloudMailUrl(config.baseUrl, path); + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(new Error('timeout')), timeoutMs); + let response; + try { + response = await fetchImpl(url, { + method, + headers: buildCloudMailHeaders(config, { + json: payload !== undefined, + token: requireToken ? undefined : '', + }), + body: payload !== undefined ? JSON.stringify(payload) : undefined, + signal: controller.signal, + }); + } catch (err) { + const errorMessage = err?.name === 'AbortError' + ? `Cloud Mail 请求超时(>${Math.round(timeoutMs / 1000)} 秒)` + : `Cloud Mail 请求失败:${err.message}`; + throw new Error(errorMessage); + } finally { + clearTimeout(timeoutId); + } + const text = await response.text(); + let parsed; + try { + parsed = text ? JSON.parse(text) : {}; + } catch { + parsed = text; + } + if (!response.ok) { + const payloadError = typeof parsed === 'object' && parsed + ? (parsed.message || parsed.error || parsed.msg) + : ''; + throw new Error(`Cloud Mail 请求失败:${payloadError || text || `HTTP ${response.status}`}`); + } + if (parsed && typeof parsed === 'object' && 'code' in parsed && Number(parsed.code) !== 200) { + throw new Error(`Cloud Mail 业务错误:${parsed.message || parsed.msg || `code=${parsed.code}`}`); + } + return parsed; + } + + async function ensureCloudMailToken(state, options = {}) { + const { forceRefresh = false } = options; + const latestState = state || await getState(); + const config = ensureCloudMailConfig(latestState, { requireCredentials: true }); + if (!forceRefresh && config.token) { + return { config, token: config.token }; + } + const loginConfig = { ...config, token: '' }; + const result = await requestCloudMailJson(loginConfig, '/api/public/genToken', { + method: 'POST', + payload: { email: config.adminEmail, password: config.adminPassword }, + requireToken: false, + }); + const token = getCloudMailTokenFromResponse(result); + if (!token) { + throw new Error('Cloud Mail 未返回可用 Token。'); + } + await setPersistentSettings({ cloudMailToken: token }); + return { config: { ...config, token }, token }; + } + + function generateCloudMailAliasLocalPart() { + const letters = 'abcdefghijklmnopqrstuvwxyz'; + const digits = '0123456789'; + const chars = []; + for (let i = 0; i < 6; i++) chars.push(letters[Math.floor(Math.random() * letters.length)]); + for (let i = 0; i < 4; i++) chars.push(digits[Math.floor(Math.random() * digits.length)]); + for (let i = chars.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [chars[i], chars[j]] = [chars[j], chars[i]]; + } + return chars.join(''); + } + + async function fetchCloudMailAddress(state, options = {}) { + throwIfStopped(); + const latestState = state || await getState(); + const { config } = await ensureCloudMailToken(latestState); + const ensuredConfig = ensureCloudMailConfig({ ...latestState, cloudMailToken: config.token }, { + requireToken: true, + requireDomain: true, + }); + const requestedLocal = String(options.localPart || options.name || '').trim().toLowerCase() + || generateCloudMailAliasLocalPart(); + const address = `${requestedLocal}@${ensuredConfig.domain}`.toLowerCase(); + const payload = { list: [{ email: address }] }; + try { + await requestCloudMailJson(ensuredConfig, '/api/public/addUser', { method: 'POST', payload }); + } catch (err) { + if (/token|unauthor|401/i.test(String(err?.message || ''))) { + const refreshed = await ensureCloudMailToken(latestState, { forceRefresh: true }); + await requestCloudMailJson(refreshed.config, '/api/public/addUser', { method: 'POST', payload }); + } else { + throw err; + } + } + await setEmailState(address); + await addLog(`Cloud Mail:已生成 ${address}`, 'ok'); + return address; + } + + function summarizeCloudMailMessagesForLog(messages) { + return (messages || []) + .slice() + .sort((left, right) => { + const leftTime = Date.parse(left.receivedDateTime || '') || 0; + const rightTime = Date.parse(right.receivedDateTime || '') || 0; + return rightTime - leftTime; + }) + .slice(0, 3) + .map((message) => { + const receivedAt = message?.receivedDateTime || '未知时间'; + const sender = message?.from?.emailAddress?.address || '未知发件人'; + const subject = message?.subject || '(无主题)'; + const preview = String(message?.bodyPreview || '').replace(/\s+/g, ' ').trim().slice(0, 80); + const address = message?.address || '未知地址'; + return `[${address}] ${receivedAt} | ${sender} | ${subject} | ${preview}`; + }) + .join(' || '); + } + + async function listCloudMailMessages(state, options = {}) { + const latestState = state || await getState(); + const { config } = await ensureCloudMailToken(latestState); + const address = normalizeCloudMailAddress(options.address); + const pageSize = Number(options.limit) || CLOUD_MAIL_DEFAULT_PAGE_SIZE; + const pageNum = Number(options.page) || 1; + const request = async (currentConfig) => requestCloudMailJson(currentConfig, '/api/public/emailList', { + method: 'POST', + payload: { + toEmail: address || undefined, + type: 0, + isDel: 0, + timeSort: 'desc', + num: pageNum, + size: pageSize, + }, + }); + let payload; + try { + payload = await request(config); + } catch (err) { + if (/token|unauthor|401/i.test(String(err?.message || ''))) { + const refreshed = await ensureCloudMailToken(latestState, { forceRefresh: true }); + payload = await request(refreshed.config); + } else { + throw err; + } + } + const messages = normalizeCloudMailMailApiMessages(payload).filter((message) => { + if (!address) return true; + return !message.address || normalizeCloudMailAddress(message.address) === address; + }); + return { config, messages }; + } + + async function pollCloudMailVerificationCode(step, state, pollPayload = {}) { + const latestState = state || await getState(); + const config = ensureCloudMailConfig(latestState, { requireCredentials: true }); + const targetEmail = resolveCloudMailPollTargetEmail(latestState, pollPayload, config); + const registrationEmail = normalizeCloudMailReceiveMailbox(latestState.email); + if (!targetEmail) { + throw new Error('Cloud Mail 轮询前缺少目标邮箱地址,请先填写注册邮箱或"邮件接收"邮箱。'); + } + if (registrationEmail && registrationEmail !== targetEmail) { + await addLog(`步骤 ${step}:正在轮询 Cloud Mail 收件邮箱(${targetEmail}),注册邮箱为 ${registrationEmail}...`, 'info'); + } else { + await addLog(`步骤 ${step}:正在轮询 Cloud Mail 邮件(${targetEmail})...`, 'info'); + } + const maxAttempts = Number(pollPayload.maxAttempts) || 5; + const intervalMs = Number(pollPayload.intervalMs) || 3000; + let lastError = null; + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + throwIfStopped(); + try { + const { messages } = await listCloudMailMessages(latestState, { + address: targetEmail, + limit: pollPayload.limit || CLOUD_MAIL_DEFAULT_PAGE_SIZE, + page: pollPayload.page || 1, + }); + const matchResult = pickVerificationMessageWithTimeFallback(messages, { + afterTimestamp: pollPayload.filterAfterTimestamp || 0, + senderFilters: pollPayload.senderFilters || [], + subjectFilters: pollPayload.subjectFilters || [], + excludeCodes: pollPayload.excludeCodes || [], + }); + const match = matchResult.match; + if (match?.code) { + if (matchResult.usedRelaxedFilters) { + const fallbackLabel = matchResult.usedTimeFallback ? '宽松匹配 + 时间回退' : '宽松匹配'; + await addLog(`步骤 ${step}:严格规则未命中,已改用 ${fallbackLabel} 并命中 Cloud Mail 验证码。`, 'warn'); + } + return { + ok: true, + code: match.code, + emailTimestamp: match.receivedAt || Date.now(), + mailId: match.message?.id || '', + }; + } + lastError = new Error(`步骤 ${step}:暂未在 Cloud Mail 中找到匹配验证码(${attempt}/${maxAttempts})。`); + await addLog(lastError.message, attempt === maxAttempts ? 'warn' : 'info'); + const sample = summarizeCloudMailMessagesForLog(messages); + if (sample) { + await addLog(`步骤 ${step}:最近邮件样本:${sample}`, 'info'); + } + } catch (err) { + lastError = err; + await addLog(`步骤 ${step}:Cloud Mail 轮询失败:${err.message}`, 'warn'); + } + if (attempt < maxAttempts) { + await sleepWithStop(intervalMs); + } + } + throw lastError || new Error(`步骤 ${step}:未在 Cloud Mail 中找到新的匹配验证码。`); + } + + return { + ensureCloudMailConfig, + ensureCloudMailToken, + fetchCloudMailAddress, + getCloudMailConfig, + listCloudMailMessages, + normalizeCloudMailReceiveMailbox, + pollCloudMailVerificationCode, + requestCloudMailJson, + resolveCloudMailPollTargetEmail, + }; + } + + return { + createCloudMailProvider, + }; +}); diff --git a/background/logging-status.js b/background/logging-status.js index f6a21e4..06a3145 100644 --- a/background/logging-status.js +++ b/background/logging-status.js @@ -27,6 +27,7 @@ 'hotmail-api': 'Hotmail(API对接/本地助手)', 'luckmail-api': 'LuckMail(API 购邮)', 'cloudflare-temp-email': 'Cloudflare Temp Email', + 'cloudmail': 'Cloud Mail', }; return labels[source] || source || '未知来源'; } diff --git a/background/phone-verification-flow.js b/background/phone-verification-flow.js index ee951e0..62331d4 100644 --- a/background/phone-verification-flow.js +++ b/background/phone-verification-flow.js @@ -100,6 +100,16 @@ const FREE_PHONE_REUSE_PREPARE_MAX_ROUNDS = 10; const PHONE_SMS_FAILURE_SKIP_THRESHOLD = 2; const MAX_ACTIVATION_PRICE_HINTS = 256; + const HERO_SMS_COUNTRY_BY_PHONE_PREFIX = Object.freeze([ + { prefix: '84', id: 10, label: 'Vietnam' }, + { prefix: '66', id: 52, label: 'Thailand' }, + { prefix: '62', id: 6, label: 'Indonesia' }, + { prefix: '44', id: 16, label: 'United Kingdom' }, + { prefix: '81', id: 151, label: 'Japan' }, + { prefix: '49', id: 43, label: 'Germany' }, + { prefix: '33', id: 73, label: 'France' }, + { prefix: '1', id: 187, label: 'USA' }, + ]); const activationPriceHintsByKey = new Map(); let activePhoneVerificationLogStep = null; let activePhoneVerificationLogStepKey = null; @@ -422,6 +432,21 @@ return String(value || '').trim() || fallback; } + function inferHeroSmsCountryFromPhoneNumber(phoneNumber = '') { + const digits = String(phoneNumber || '').replace(/\D+/g, ''); + if (!digits) { + return null; + } + const match = HERO_SMS_COUNTRY_BY_PHONE_PREFIX.find((entry) => digits.startsWith(entry.prefix)); + if (!match) { + return null; + } + return { + id: normalizeCountryId(match.id, 0), + label: normalizeCountryLabel(match.label, `Country #${match.id}`), + }; + } + function normalizePhoneCodeWaitSeconds(value) { const parsed = Math.floor(Number(value)); if (!Number.isFinite(parsed) || parsed <= 0) { @@ -1037,14 +1062,19 @@ const activationId = String( record.activationId ?? record.id ?? record.activation ?? '' ).trim(); - const countryLabel = String(record.countryLabel || '').trim(); + const inferredCountry = inferHeroSmsCountryFromPhoneNumber(phoneNumber); + const countryId = normalizeCountryId(record.countryId, inferredCountry?.id || HERO_SMS_COUNTRY_ID); + const countryLabel = String( + record.countryLabel + || (inferredCountry && inferredCountry.id === countryId ? inferredCountry.label : '') + ).trim(); const statusAction = String(record.statusAction || '').trim(); return { ...(activationId ? { activationId } : {}), phoneNumber, provider: PHONE_SMS_PROVIDER_HERO, serviceCode: String(record.serviceCode || HERO_SMS_SERVICE_CODE).trim() || HERO_SMS_SERVICE_CODE, - countryId: normalizeCountryId(record.countryId, HERO_SMS_COUNTRY_ID), + countryId, ...(countryLabel ? { countryLabel } : {}), successfulUses: normalizeUseCount(record.successfulUses), maxUses: Math.max(1, Math.floor(Number(record.maxUses) || DEFAULT_PHONE_NUMBER_MAX_USES)), @@ -3935,7 +3965,16 @@ }; } } else { - const countryId = normalizeCountryId(activation.countryId, 0); + const inferredCountry = inferHeroSmsCountryFromPhoneNumber(activation.phoneNumber); + const rawCountryId = normalizeCountryId(activation.countryId, 0); + const hasExplicitCountry = Object.prototype.hasOwnProperty.call(activation, 'countryId') + && Number.isFinite(rawCountryId) + && rawCountryId > 0 + && !(activation.manualOnly && rawCountryId === HERO_SMS_COUNTRY_ID && inferredCountry?.id && inferredCountry.id !== rawCountryId); + const countryId = hasExplicitCountry ? rawCountryId : normalizeCountryId(inferredCountry?.id, rawCountryId); + const countryLabel = hasExplicitCountry + ? activation.countryLabel + : (inferredCountry?.label || activation.countryLabel); if (countryId > 0) { const matched = candidates.find((entry) => entry.id === countryId); if (matched) { @@ -3943,7 +3982,7 @@ } return { id: countryId, - label: normalizeCountryLabel(activation.countryLabel, `Country #${countryId}`), + label: normalizeCountryLabel(countryLabel, `Country #${countryId}`), }; } } @@ -4286,7 +4325,11 @@ return null; } - if (normalizeFreePhoneReuseAutoEnabled(state)) { + const canPrepareAutomaticFreeReuse = normalizeFreePhoneReuseAutoEnabled(state) + && !freeReusableActivation.manualOnly + && Boolean(String(freeReusableActivation.activationId || '').trim()); + + if (canPrepareAutomaticFreeReuse) { await addLog( `步骤 9:准备自动白嫖复用已保存手机号 ${freeReusableActivation.phoneNumber}(${freeReusableActivation.successfulUses + 1}/${freeReusableActivation.maxUses})。`, 'info' @@ -6245,6 +6288,7 @@ completeSignupPhoneVerificationFlow, finalizeLoginPhoneActivationAfterSuccess, finalizeSignupPhoneActivationAfterSuccess, + isPhoneResendBannedNumberError, normalizeActivation, pollPhoneActivationCode, prepareLoginPhoneActivation, diff --git a/background/steps/fetch-login-code.js b/background/steps/fetch-login-code.js index b0c7fcc..ea7c81d 100644 --- a/background/steps/fetch-login-code.js +++ b/background/steps/fetch-login-code.js @@ -10,6 +10,7 @@ addLog: rawAddLog = async () => {}, chrome, CLOUDFLARE_TEMP_EMAIL_PROVIDER, + CLOUD_MAIL_PROVIDER = 'cloudmail', completeStepFromBackground, confirmCustomVerificationStepBypass, ensureMail2925MailboxSession, @@ -484,6 +485,7 @@ mail.provider === HOTMAIL_PROVIDER || mail.provider === LUCKMAIL_PROVIDER || mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER + || mail.provider === CLOUD_MAIL_PROVIDER ) { await addLog(`步骤 ${visibleStep}:正在通过 ${mail.label} 轮询验证码...`); } else { diff --git a/background/steps/fetch-signup-code.js b/background/steps/fetch-signup-code.js index 0d2051f..3f55172 100644 --- a/background/steps/fetch-signup-code.js +++ b/background/steps/fetch-signup-code.js @@ -19,6 +19,7 @@ isTabAlive, LUCKMAIL_PROVIDER, CLOUDFLARE_TEMP_EMAIL_PROVIDER, + CLOUD_MAIL_PROVIDER = 'cloudmail', resolveVerificationStep, reuseOrCreateTab, sendToContentScript, @@ -238,6 +239,7 @@ mail.provider === HOTMAIL_PROVIDER || mail.provider === LUCKMAIL_PROVIDER || mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER + || mail.provider === CLOUD_MAIL_PROVIDER ) { await addLog(`步骤 4:正在通过 ${mail.label} 轮询验证码...`); } else if (mail.provider === '2925') { @@ -263,6 +265,7 @@ HOTMAIL_PROVIDER, LUCKMAIL_PROVIDER, CLOUDFLARE_TEMP_EMAIL_PROVIDER, + CLOUD_MAIL_PROVIDER, ].includes(mail.provider); const signupProfile = buildSignupProfileForVerificationStep(); diff --git a/background/verification-flow.js b/background/verification-flow.js index fe39705..d3fd5e5 100644 --- a/background/verification-flow.js +++ b/background/verification-flow.js @@ -10,6 +10,7 @@ chrome, closeConflictingTabsForSource, CLOUDFLARE_TEMP_EMAIL_PROVIDER, + CLOUD_MAIL_PROVIDER = 'cloudmail', completeStepFromBackground, confirmCustomVerificationStepBypassRequest, getHotmailVerificationPollConfig, @@ -24,6 +25,7 @@ MAIL_2925_VERIFICATION_INTERVAL_MS, MAIL_2925_VERIFICATION_MAX_ATTEMPTS, pollCloudflareTempEmailVerificationCode, + pollCloudMailVerificationCode, pollHotmailVerificationCode, pollLuckmailVerificationCode, sendToContentScript, @@ -927,6 +929,13 @@ }, cleanPollOverrides, `轮询${getVerificationCodeLabel(step)}验证码邮箱`); return pollCloudflareTempEmailVerificationCode(step, state, timedPoll.payload); } + if (mail.provider === CLOUD_MAIL_PROVIDER) { + const timedPoll = await applyMailPollingTimeBudget(step, { + ...getVerificationPollPayload(step, state), + ...cleanPollOverrides, + }, cleanPollOverrides, `轮询${getVerificationCodeLabel(step)}验证码邮箱`); + return pollCloudMailVerificationCode(step, state, timedPoll.payload); + } if (Number(pollOverrides.resendIntervalMs) > 0) { return pollFreshVerificationCodeWithResendInterval(step, state, mail, pollOverrides); diff --git a/cloudmail-utils.js b/cloudmail-utils.js new file mode 100644 index 0000000..f43d0f9 --- /dev/null +++ b/cloudmail-utils.js @@ -0,0 +1,228 @@ +(function cloudMailUtilsModule(root, factory) { + if (typeof module !== 'undefined' && module.exports) { + module.exports = factory(); + return; + } + + root.CloudMailUtils = factory(); +})(typeof self !== 'undefined' ? self : globalThis, function createCloudMailUtils() { + const DEFAULT_MAIL_PAGE_SIZE = 20; + + function firstNonEmptyString(values) { + for (const value of values) { + if (value === undefined || value === null) continue; + const normalized = String(value).trim(); + if (normalized) return normalized; + } + return ''; + } + + function normalizeCloudMailBaseUrl(rawValue = '') { + const value = String(rawValue || '').trim(); + if (!value) return ''; + + const candidate = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(value) ? value : `https://${value}`; + try { + const parsed = new URL(candidate); + parsed.hash = ''; + parsed.search = ''; + const pathname = parsed.pathname === '/' ? '' : parsed.pathname.replace(/\/+$/, ''); + return `${parsed.origin}${pathname}`; + } catch { + return ''; + } + } + + function normalizeCloudMailDomain(rawValue = '') { + let value = String(rawValue || '').trim().toLowerCase(); + if (!value) return ''; + value = value.replace(/^@+/, ''); + value = value.replace(/^https?:\/\//, ''); + value = value.replace(/\/.*$/, ''); + if (!/^[a-z0-9.-]+\.[a-z]{2,}$/i.test(value)) { + return ''; + } + return value; + } + + function normalizeCloudMailDomains(values) { + const domains = []; + const seen = new Set(); + for (const value of Array.isArray(values) ? values : []) { + const normalized = normalizeCloudMailDomain(value); + if (!normalized || seen.has(normalized)) continue; + seen.add(normalized); + domains.push(normalized); + } + return domains; + } + + function buildCloudMailHeaders(config = {}, options = {}) { + const headers = {}; + const token = firstNonEmptyString([ + config.token, + config.cloudMailToken, + options.token, + ]); + if (token) { + headers.Authorization = token; + } + if (options.json) { + headers['Content-Type'] = 'application/json'; + } + if (options.acceptJson !== false) { + headers.Accept = 'application/json'; + } + return headers; + } + + function joinCloudMailUrl(baseUrl, path) { + const normalizedBase = normalizeCloudMailBaseUrl(baseUrl); + const normalizedPath = String(path || '').trim(); + if (!normalizedBase || !normalizedPath) return normalizedBase || ''; + return `${normalizedBase}${normalizedPath.startsWith('/') ? '' : '/'}${normalizedPath}`; + } + + function getCloudMailMailRows(payload) { + if (Array.isArray(payload)) return payload; + if (!payload || typeof payload !== 'object') return []; + + const candidates = [ + payload.data, + payload.list, + payload.items, + payload.rows, + payload.records, + payload?.data?.list, + payload?.data?.records, + payload?.data?.rows, + ]; + + for (const candidate of candidates) { + if (Array.isArray(candidate)) { + return candidate; + } + } + + return []; + } + + function normalizeCloudMailAddress(value) { + return String(value || '').trim().toLowerCase(); + } + + function stripHtmlTags(value = '') { + return String(value || '') + .replace(//gi, ' ') + .replace(//gi, ' ') + .replace(/<[^>]+>/g, ' ') + .replace(/ /gi, ' ') + .replace(/&/gi, '&') + .replace(/</gi, '<') + .replace(/>/gi, '>') + .replace(/\s+/g, ' ') + .trim(); + } + + function parseCloudMailCreateTime(value) { + if (value === undefined || value === null || value === '') return ''; + if (typeof value === 'number' && Number.isFinite(value)) { + return new Date(value).toISOString(); + } + const source = String(value).trim(); + if (!source) return ''; + + // Cloud Mail returns UTC time like "2099-12-30 23:59:59"; treat as UTC. + const match = source.match(/^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?(?:Z|([+-]\d{2}:?\d{2}))?$/); + if (match) { + const [, year, month, day, hour, minute, second, ms, offset] = match; + let iso = `${year}-${month}-${day}T${hour}:${minute}:${second}`; + if (ms) iso += `.${ms}`; + if (offset) { + iso += offset.includes(':') ? offset : `${offset.slice(0, 3)}:${offset.slice(3)}`; + } else { + iso += 'Z'; + } + const parsed = Date.parse(iso); + return Number.isFinite(parsed) ? new Date(parsed).toISOString() : iso; + } + + const parsed = Date.parse(source); + return Number.isFinite(parsed) ? new Date(parsed).toISOString() : source; + } + + function normalizeCloudMailMessage(row = {}) { + if (!row || typeof row !== 'object') return null; + + const address = normalizeCloudMailAddress(firstNonEmptyString([ + row.toEmail, + row.to_email, + row.recipient, + row.address, + row.email, + ])); + const subject = firstNonEmptyString([row.subject, row.title]); + const fromAddress = firstNonEmptyString([ + row.sendEmail, + row.send_email, + row.from, + row.sender, + row.mailFrom, + ]); + const htmlContent = firstNonEmptyString([row.content, row.html]); + const textContent = firstNonEmptyString([row.text, row.plainText, row.content_text]); + const bodyPreview = (textContent + || stripHtmlTags(htmlContent) + || '').replace(/\s+/g, ' ').trim(); + + return { + id: firstNonEmptyString([row.emailId, row.id, row.mailId, row.mail_id]), + address, + addressId: '', + subject, + from: { + emailAddress: { + address: fromAddress, + }, + }, + bodyPreview, + raw: htmlContent || textContent || '', + receivedDateTime: parseCloudMailCreateTime(firstNonEmptyString([ + row.createTime, + row.create_time, + row.createdAt, + row.created_at, + row.receivedDateTime, + row.date, + ])), + }; + } + + function normalizeCloudMailMailApiMessages(payload) { + return getCloudMailMailRows(payload) + .map((row) => normalizeCloudMailMessage(row)) + .filter(Boolean); + } + + function getCloudMailTokenFromResponse(payload = {}) { + return firstNonEmptyString([ + payload?.data?.token, + payload?.token, + payload?.data?.accessToken, + payload?.accessToken, + ]); + } + + return { + DEFAULT_MAIL_PAGE_SIZE, + buildCloudMailHeaders, + getCloudMailTokenFromResponse, + joinCloudMailUrl, + normalizeCloudMailAddress, + normalizeCloudMailBaseUrl, + normalizeCloudMailDomain, + normalizeCloudMailDomains, + normalizeCloudMailMailApiMessages, + normalizeCloudMailMessage, + }; +}); diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index 9d7b0b2..4a4e5bc 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -414,6 +414,7 @@ + @@ -439,6 +440,7 @@ + +