diff --git a/background.js b/background.js index b107b68..f6a1e20 100644 --- a/background.js +++ b/background.js @@ -63,7 +63,9 @@ importScripts( 'luckmail-utils.js', 'cloudflare-temp-email-utils.js', 'cloudmail-utils.js', + 'freemail-utils.js', 'background/cloudmail-provider.js', + 'background/freemail-provider.js', 'icloud-utils.js', 'mail-provider-utils.js', 'content/activation-utils.js' @@ -318,6 +320,17 @@ const { normalizeCloudMailDomains, normalizeCloudMailMailApiMessages, } = self.CloudMailUtils; +const { + DEFAULT_MAIL_PAGE_SIZE: FREEMAIL_DEFAULT_PAGE_SIZE, + buildFreemailHeaders, + getFreemailAddressFromResponse, + joinFreemailUrl, + normalizeFreemailAddress, + normalizeFreemailBaseUrl, + normalizeFreemailDomain, + normalizeFreemailDomains, + normalizeFreemailMessages, +} = self.FreemailUtils; const { findIcloudAliasByEmail, getConfiguredIcloudHostPreference, @@ -462,6 +475,8 @@ 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 FREEMAIL_PROVIDER = 'freemail'; +const FREEMAIL_GENERATOR = 'freemail'; const CUSTOM_EMAIL_POOL_GENERATOR = 'custom-pool'; const HOTMAIL_MAILBOXES = ['INBOX', 'Junk']; const STOP_ERROR_MESSAGE = '流程已被用户停止。'; @@ -1117,6 +1132,11 @@ const PERSISTED_SETTING_DEFAULTS = { cloudMailReceiveMailbox: '', cloudMailDomain: '', cloudMailDomains: [], + freemailBaseUrl: '', + freemailAdminUsername: '', + freemailAdminPassword: '', + freemailDomain: '', + freemailDomains: [], hotmailAccounts: [], hotmailAliasEnabled: false, outlookAliasMaxPerAccount: OUTLOOK_ALIAS_DEFAULT_MAX_PER_ACCOUNT, @@ -2321,6 +2341,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'; + if (normalized === FREEMAIL_GENERATOR) return FREEMAIL_GENERATOR; return 'duck'; } @@ -2622,6 +2643,7 @@ function normalizeMailProvider(value = '') { case LUCKMAIL_PROVIDER: case CLOUDFLARE_TEMP_EMAIL_PROVIDER: case CLOUD_MAIL_PROVIDER: + case FREEMAIL_PROVIDER: case '163': case '163-vip': case '126': @@ -2860,6 +2882,35 @@ const { resolveCloudMailPollTargetEmail, } = cloudMailProvider; +const freemailProvider = self.MultiPageBackgroundFreemailProvider.createFreemailProvider({ + addLog, + buildFreemailHeaders, + fetchImpl: typeof fetch === 'function' ? fetch.bind(globalThis) : null, + FREEMAIL_DEFAULT_PAGE_SIZE, + FREEMAIL_GENERATOR, + FREEMAIL_PROVIDER, + getFreemailAddressFromResponse, + getState, + joinFreemailUrl, + normalizeFreemailAddress, + normalizeFreemailBaseUrl, + normalizeFreemailDomain, + normalizeFreemailDomains, + normalizeFreemailMessages, + persistRegistrationEmailState, + pickVerificationMessageWithTimeFallback, + setEmailState, + setPersistentSettings, + sleepWithStop, + throwIfStopped, +}); +const { + getFreemailConfig, + fetchFreemailAddress, + pollFreemailVerificationCode, + resolveFreemailPollTargetEmail, +} = freemailProvider; + function normalizeSub2ApiGroupNames(value = '') { const source = Array.isArray(value) ? value @@ -3240,6 +3291,9 @@ function normalizePersistentSettingValue(key, value) { if (normalizedMailProvider === CLOUD_MAIL_PROVIDER) { return CLOUD_MAIL_PROVIDER; } + if (normalizedMailProvider === FREEMAIL_PROVIDER) { + return FREEMAIL_PROVIDER; + } return HOTMAIL_PROVIDER; } case 'mail2925Mode': @@ -3330,6 +3384,16 @@ function normalizePersistentSettingValue(key, value) { return normalizeCloudMailDomain(value); case 'cloudMailDomains': return normalizeCloudMailDomains(value); + case 'freemailBaseUrl': + return normalizeFreemailBaseUrl(value); + case 'freemailAdminUsername': + return String(value || '').trim(); + case 'freemailAdminPassword': + return String(value || ''); + case 'freemailDomain': + return normalizeFreemailDomain(value); + case 'freemailDomains': + return normalizeFreemailDomains(value); case 'hotmailAccounts': return normalizeHotmailAccounts(value); case 'hotmailAliasEnabled': @@ -3478,6 +3542,13 @@ function buildPersistentSettingsPayload(input = {}, options = {}) { } payload.cloudMailDomains = domains; } + if (payload.freemailDomains) { + const domains = normalizeFreemailDomains(payload.freemailDomains); + if (payload.freemailDomain && !domains.includes(payload.freemailDomain)) { + domains.unshift(payload.freemailDomain); + } + payload.freemailDomains = domains; + } if ( Object.prototype.hasOwnProperty.call(payload, 'sub2apiGroupName') || Object.prototype.hasOwnProperty.call(payload, 'sub2apiGroupNames') @@ -11850,6 +11921,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'; + if (generator === FREEMAIL_GENERATOR) return 'freemail'; return 'Duck 邮箱'; } const mail2925SessionManager = self.MultiPageBackgroundMail2925Session?.createMail2925SessionManager({ @@ -12026,10 +12098,20 @@ async function fetchDuckEmail(options = {}) { async function fetchGeneratedEmail(state, options = {}) { const currentState = state || await getState(); + const mergedState = { + ...currentState, + freemailBaseUrl: options.freemailBaseUrl ?? currentState.freemailBaseUrl, + freemailAdminUsername: options.freemailAdminUsername ?? currentState.freemailAdminUsername, + freemailAdminPassword: options.freemailAdminPassword ?? currentState.freemailAdminPassword, + freemailDomain: options.freemailDomain ?? currentState.freemailDomain, + }; const generator = normalizeEmailGenerator(options.generator ?? currentState.emailGenerator); if (generator === CLOUD_MAIL_GENERATOR) { return fetchCloudMailAddress(currentState, options); } + if (generator === FREEMAIL_GENERATOR) { + return fetchFreemailAddress(mergedState, options); + } return generatedEmailHelpers.fetchGeneratedEmail(state, options); } @@ -13520,6 +13602,7 @@ const verificationFlowHelpers = self.MultiPageBackgroundVerificationFlow?.create closeConflictingTabsForSource, CLOUDFLARE_TEMP_EMAIL_PROVIDER, CLOUD_MAIL_PROVIDER, + FREEMAIL_PROVIDER, completeNodeFromBackground, confirmCustomVerificationStepBypassRequest: (step) => chrome.runtime.sendMessage({ type: 'REQUEST_CUSTOM_VERIFICATION_BYPASS_CONFIRMATION', @@ -13540,6 +13623,7 @@ const verificationFlowHelpers = self.MultiPageBackgroundVerificationFlow?.create MAIL_2925_VERIFICATION_MAX_ATTEMPTS, pollCloudflareTempEmailVerificationCode, pollCloudMailVerificationCode, + pollFreemailVerificationCode, pollHotmailVerificationCode, pollLuckmailVerificationCode, sendToContentScript, @@ -13671,6 +13755,7 @@ const step4Executor = self.MultiPageBackgroundStep4?.createStep4Executor({ LUCKMAIL_PROVIDER, CLOUDFLARE_TEMP_EMAIL_PROVIDER, CLOUD_MAIL_PROVIDER, + FREEMAIL_PROVIDER, resolveVerificationStep: verificationFlowHelpers.resolveVerificationStep, reuseOrCreateTab, sendToContentScript, @@ -13729,6 +13814,7 @@ const step8Executor = self.MultiPageBackgroundStep8?.createStep8Executor({ chrome, CLOUDFLARE_TEMP_EMAIL_PROVIDER, CLOUD_MAIL_PROVIDER, + FREEMAIL_PROVIDER, completeNodeFromBackground, confirmCustomVerificationStepBypass: verificationFlowHelpers.confirmCustomVerificationStepBypass, ensureMail2925MailboxSession, @@ -14346,6 +14432,9 @@ function getMailConfig(state) { if (provider === 'cloudmail') { return { provider: 'cloudmail', label: 'Cloud Mail' }; } + if (provider === FREEMAIL_PROVIDER) { + return { provider: FREEMAIL_PROVIDER, label: 'freemail' }; + } 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/freemail-provider.js b/background/freemail-provider.js new file mode 100644 index 0000000..892b9fd --- /dev/null +++ b/background/freemail-provider.js @@ -0,0 +1,263 @@ +(function freemailProviderModule(root, factory) { + root.MultiPageBackgroundFreemailProvider = factory(); +})(typeof self !== 'undefined' ? self : globalThis, function createFreemailProviderModule() { + function createFreemailProvider(deps = {}) { + const { + addLog = async () => {}, + buildFreemailHeaders, + fetchImpl = typeof fetch === 'function' ? fetch.bind(globalThis) : null, + FREEMAIL_DEFAULT_PAGE_SIZE = 20, + FREEMAIL_GENERATOR = 'freemail', + FREEMAIL_PROVIDER = 'freemail', + getFreemailAddressFromResponse, + getState = async () => ({}), + joinFreemailUrl, + normalizeFreemailAddress, + normalizeFreemailBaseUrl, + normalizeFreemailDomain, + normalizeFreemailDomains, + normalizeFreemailMessages, + persistRegistrationEmailState = null, + pickVerificationMessageWithTimeFallback, + setEmailState = async () => {}, + setPersistentSettings = async () => {}, + sleepWithStop = async () => {}, + throwIfStopped = () => {}, + } = deps; + + function normalizeFreemailReceiveMailbox(value = '') { + const normalized = normalizeFreemailAddress(value); + if (!normalized) return ''; + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalized) ? normalized : ''; + } + + function getFreemailConfig(state = {}) { + return { + baseUrl: normalizeFreemailBaseUrl(state.freemailBaseUrl), + adminUsername: String(state.freemailAdminUsername || '').trim(), + adminPassword: String(state.freemailAdminPassword || ''), + domain: normalizeFreemailDomain(state.freemailDomain), + domains: normalizeFreemailDomains(state.freemailDomains), + }; + } + + function ensureFreemailConfig(state, options = {}) { + const { requireAuth = false, requireDomain = false } = options; + const config = getFreemailConfig(state); + if (!config.baseUrl) throw new Error('freemail 服务地址为空或格式无效。'); + if (requireAuth && (!config.adminUsername || !config.adminPassword)) { + throw new Error('freemail 缺少管理员账号或密码。'); + } + if (requireDomain && !config.domain) { + throw new Error('freemail 域名为空或格式无效。'); + } + return config; + } + + async function requestFreemailJson(config, path, options = {}) { + if (!fetchImpl) throw new Error('freemail 当前运行环境不支持 fetch。'); + const { method = 'GET', payload, searchParams, timeoutMs = 20000 } = options; + const url = new URL(joinFreemailUrl(config.baseUrl, path)); + if (searchParams && typeof searchParams === 'object') { + for (const [key, value] of Object.entries(searchParams)) { + if (value === undefined || value === null || value === '') continue; + url.searchParams.set(key, String(value)); + } + } + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(new Error('timeout')), timeoutMs); + let response; + try { + response = await fetchImpl(url.toString(), { + method, + credentials: 'include', + headers: buildFreemailHeaders(config, { json: payload !== undefined }), + body: payload !== undefined ? JSON.stringify(payload) : undefined, + signal: controller.signal, + }); + } catch (err) { + const message = err?.name === 'AbortError' + ? `freemail 请求超时(>${Math.round(timeoutMs / 1000)} 秒)` + : `freemail 请求失败:${err.message}`; + throw new Error(message); + } 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( + `freemail 请求失败:${payloadError || text || `HTTP ${response.status}`}` + ); + } + return parsed; + } + + async function ensureFreemailSession(state) { + const config = ensureFreemailConfig(state || await getState(), { requireAuth: true }); + const result = await requestFreemailJson(config, '/api/login', { + method: 'POST', + payload: { username: config.adminUsername, password: config.adminPassword }, + }); + if (result?.success === false) throw new Error('freemail 管理员登录失败。'); + return config; + } + + async function persistResolvedEmailState(state = null, email, options = {}) { + if (typeof persistRegistrationEmailState === 'function') { + await persistRegistrationEmailState(state, email, options); + return; + } + await setEmailState(email, options); + } + + async function fetchFreemailDomains(state = null, sessionConfig = null) { + const latestState = state || await getState(); + const config = sessionConfig || await ensureFreemailSession(latestState); + const payload = await requestFreemailJson(config, '/api/domains'); + const domains = normalizeFreemailDomains(payload); + if (domains.length) { + await setPersistentSettings({ + freemailDomains: domains, + freemailDomain: domains[0], + }); + } + return domains; + } + + async function fetchFreemailAddress(state, options = {}) { + throwIfStopped(); + const latestState = state || await getState(); + const config = await ensureFreemailSession(latestState); + const domains = config.domains.length + ? config.domains + : await fetchFreemailDomains(latestState, config); + const activeDomain = config.domain || domains[0] || ''; + const domainIndex = domains.indexOf(activeDomain); + if (activeDomain && domainIndex < 0) { + throw new Error( + `freemail 主域名 ${activeDomain} 不在 /api/domains 返回列表中。` + ); + } + const local = String(options.localPart || options.name || '').trim().toLowerCase(); + const path = local ? '/api/create' : '/api/generate'; + const payload = local ? { local, domainIndex } : undefined; + const result = await requestFreemailJson(config, path, { + method: local ? 'POST' : 'GET', + payload, + searchParams: local ? undefined : { domainIndex: Math.max(0, domainIndex) }, + }); + const address = normalizeFreemailAddress(getFreemailAddressFromResponse(result)); + if (!address) throw new Error('freemail 未返回可用邮箱地址。'); + await persistResolvedEmailState(latestState, address, { + source: 'generated:freemail', + preserveAccountIdentity: Boolean(options?.preserveAccountIdentity), + }); + await addLog(`freemail:已生成 ${address}`, 'ok'); + return address; + } + + function resolveFreemailPollTargetEmail( + state = {}, + pollPayload = {}, + config = getFreemailConfig(state) + ) { + const mailProvider = String(state?.mailProvider || '').trim().toLowerCase(); + const emailGenerator = String(state?.emailGenerator || '').trim().toLowerCase(); + const shouldUseCurrentEmail = mailProvider === FREEMAIL_PROVIDER + && emailGenerator !== FREEMAIL_GENERATOR; + if (shouldUseCurrentEmail) return normalizeFreemailReceiveMailbox(state.email); + return normalizeFreemailReceiveMailbox(pollPayload.targetEmail) + || normalizeFreemailReceiveMailbox(state.email); + } + + async function listFreemailMessages(state, options = {}) { + const latestState = state || await getState(); + const config = await ensureFreemailSession(latestState); + const address = normalizeFreemailReceiveMailbox(options.address); + const payload = await requestFreemailJson(config, '/api/emails', { + searchParams: { + mailbox: address, + limit: Number(options.limit) || FREEMAIL_DEFAULT_PAGE_SIZE, + }, + }); + return { config, messages: normalizeFreemailMessages(payload) }; + } + + async function pollFreemailVerificationCode(step, state, pollPayload = {}) { + const latestState = state || await getState(); + const config = ensureFreemailConfig(latestState, { requireAuth: true }); + const targetEmail = resolveFreemailPollTargetEmail(latestState, pollPayload, config); + if (!targetEmail) { + throw new Error([ + 'freemail 轮询前缺少目标邮箱地址,', + '请先生成或填写注册邮箱。', + ].join('')); + } + await addLog(`步骤 ${step}:正在轮询 freemail 邮件(${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 listFreemailMessages(latestState, { + address: targetEmail, + limit: pollPayload.limit || FREEMAIL_DEFAULT_PAGE_SIZE, + }); + const matchResult = pickVerificationMessageWithTimeFallback(messages, { + afterTimestamp: pollPayload.filterAfterTimestamp || 0, + senderFilters: pollPayload.senderFilters || [], + subjectFilters: pollPayload.subjectFilters || [], + requiredKeywords: pollPayload.requiredKeywords || [], + codePatterns: pollPayload.codePatterns || [], + excludeCodes: pollPayload.excludeCodes || [], + }); + const match = matchResult.match; + if (match?.code) { + return { + ok: true, + code: match.code, + emailTimestamp: match.receivedAt || Date.now(), + mailId: match.message?.id || '', + }; + } + lastError = new Error( + `步骤 ${step}:暂未在 freemail 中找到匹配验证码` + + `(${attempt}/${maxAttempts})。` + ); + await addLog(lastError.message, attempt === maxAttempts ? 'warn' : 'info'); + } catch (err) { + lastError = err; + await addLog(`步骤 ${step}:freemail 轮询失败:${err.message}`, 'warn'); + } + if (attempt < maxAttempts) await sleepWithStop(intervalMs); + } + throw lastError || new Error( + `步骤 ${step}:未在 freemail 中找到新的匹配验证码。` + ); + } + + return { + ensureFreemailConfig, + fetchFreemailAddress, + fetchFreemailDomains, + getFreemailConfig, + listFreemailMessages, + normalizeFreemailReceiveMailbox, + pollFreemailVerificationCode, + requestFreemailJson, + resolveFreemailPollTargetEmail, + }; + } + + return { createFreemailProvider }; +}); diff --git a/background/logging-status.js b/background/logging-status.js index e0c41cc..12704fb 100644 --- a/background/logging-status.js +++ b/background/logging-status.js @@ -35,6 +35,7 @@ 'luckmail-api': 'LuckMail(API 购邮)', 'cloudflare-temp-email': 'Cloudflare Temp Email', 'cloudmail': 'Cloud Mail', + 'freemail': 'freemail', 'plus-checkout': 'Plus Checkout', 'paypal-flow': 'PayPal 授权页', 'gopay-flow': 'GoPay 授权页', diff --git a/background/steps/fetch-login-code.js b/background/steps/fetch-login-code.js index 5ecd1e7..0798f45 100644 --- a/background/steps/fetch-login-code.js +++ b/background/steps/fetch-login-code.js @@ -9,6 +9,7 @@ chrome, CLOUDFLARE_TEMP_EMAIL_PROVIDER, CLOUD_MAIL_PROVIDER = 'cloudmail', + FREEMAIL_PROVIDER = 'freemail', completeNodeFromBackground, confirmCustomVerificationStepBypass, ensureMail2925MailboxSession, @@ -603,6 +604,7 @@ || mail.provider === LUCKMAIL_PROVIDER || mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER || mail.provider === CLOUD_MAIL_PROVIDER + || mail.provider === FREEMAIL_PROVIDER ) { await addLog(`步骤 ${visibleStep}:正在通过 ${mail.label} 轮询验证码...`); } else { diff --git a/background/steps/fetch-signup-code.js b/background/steps/fetch-signup-code.js index 102cc24..6143de0 100644 --- a/background/steps/fetch-signup-code.js +++ b/background/steps/fetch-signup-code.js @@ -20,6 +20,7 @@ LUCKMAIL_PROVIDER, CLOUDFLARE_TEMP_EMAIL_PROVIDER, CLOUD_MAIL_PROVIDER = 'cloudmail', + FREEMAIL_PROVIDER = 'freemail', resolveVerificationStep, reuseOrCreateTab, sendToContentScript, @@ -120,6 +121,7 @@ || mail.provider === LUCKMAIL_PROVIDER || mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER || mail.provider === CLOUD_MAIL_PROVIDER + || mail.provider === FREEMAIL_PROVIDER ) { await addLog(`步骤 4:正在通过 ${mail.label} 轮询验证码...`); } else if (mail.provider === '2925') { @@ -146,6 +148,7 @@ LUCKMAIL_PROVIDER, CLOUDFLARE_TEMP_EMAIL_PROVIDER, CLOUD_MAIL_PROVIDER, + FREEMAIL_PROVIDER, ].includes(mail.provider); const signupProfile = buildSignupProfileForVerificationStep(); diff --git a/background/verification-flow.js b/background/verification-flow.js index 8b07c40..3bf31bd 100644 --- a/background/verification-flow.js +++ b/background/verification-flow.js @@ -12,6 +12,7 @@ closeConflictingTabsForSource, CLOUDFLARE_TEMP_EMAIL_PROVIDER, CLOUD_MAIL_PROVIDER = 'cloudmail', + FREEMAIL_PROVIDER = 'freemail', completeNodeFromBackground, confirmCustomVerificationStepBypassRequest, getNodeIdByStepForState, @@ -28,6 +29,7 @@ MAIL_2925_VERIFICATION_MAX_ATTEMPTS, pollCloudflareTempEmailVerificationCode, pollCloudMailVerificationCode, + pollFreemailVerificationCode, pollHotmailVerificationCode, pollLuckmailVerificationCode, sendToContentScript, @@ -985,6 +987,13 @@ }, cleanPollOverrides, `轮询${getVerificationCodeLabel(step)}验证码邮箱`); return pollCloudMailVerificationCode(step, state, timedPoll.payload); } + if (mail.provider === FREEMAIL_PROVIDER) { + const timedPoll = await applyMailPollingTimeBudget(step, { + ...getVerificationPollPayload(step, state), + ...cleanPollOverrides, + }, cleanPollOverrides, `轮询${getVerificationCodeLabel(step)}验证码邮箱`); + return pollFreemailVerificationCode(step, state, timedPoll.payload); + } if (Number(pollOverrides.resendIntervalMs) > 0) { return pollFreshVerificationCodeWithResendInterval(step, state, mail, pollOverrides); diff --git a/freemail-utils.js b/freemail-utils.js new file mode 100644 index 0000000..e6bce8b --- /dev/null +++ b/freemail-utils.js @@ -0,0 +1,161 @@ +(function freemailUtilsModule(root, factory) { + if (typeof module !== 'undefined' && module.exports) { + module.exports = factory(); + return; + } + + root.FreemailUtils = factory(); +})(typeof self !== 'undefined' ? self : globalThis, function createFreemailUtils() { + 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 normalizeFreemailBaseUrl(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 normalizeFreemailDomain(rawValue = '') { + let value = String(rawValue || '').trim().toLowerCase(); + if (!value) return ''; + value = value.replace(/^@+/, '').replace(/^https?:\/\//, '').replace(/\/.*$/, ''); + return /^[a-z0-9.-]+\.[a-z]{2,}$/i.test(value) ? value : ''; + } + + function normalizeFreemailDomains(values) { + const domains = []; + const seen = new Set(); + for (const value of Array.isArray(values) ? values : []) { + const normalized = normalizeFreemailDomain(value); + if (!normalized || seen.has(normalized)) continue; + seen.add(normalized); + domains.push(normalized); + } + return domains; + } + + function normalizeFreemailAddress(value) { + return String(value || '').trim().toLowerCase(); + } + + function buildFreemailHeaders(config = {}, options = {}) { + const headers = {}; + if (options.json) { + headers['Content-Type'] = 'application/json'; + } + if (options.acceptJson !== false) { + headers.Accept = 'application/json'; + } + return headers; + } + + function joinFreemailUrl(baseUrl, path) { + const normalizedBase = normalizeFreemailBaseUrl(baseUrl); + const normalizedPath = String(path || '').trim(); + if (!normalizedBase || !normalizedPath) return normalizedBase || ''; + return `${normalizedBase}${normalizedPath.startsWith('/') ? '' : '/'}${normalizedPath}`; + } + + function getFreemailRows(payload) { + if (Array.isArray(payload)) return payload; + if (!payload || typeof payload !== 'object') return []; + const candidates = [payload.data, payload.items, payload.rows, payload.results, payload.list]; + for (const candidate of candidates) { + if (Array.isArray(candidate)) return candidate; + } + return []; + } + + 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 normalizeFreemailDate(value) { + if (value === undefined || value === null || value === '') return ''; + if (typeof value === 'number' && Number.isFinite(value)) return new Date(value).toISOString(); + const parsed = Date.parse(String(value).trim().replace(' ', 'T')); + return Number.isFinite(parsed) ? new Date(parsed).toISOString() : String(value).trim(); + } + + function normalizeFreemailMessage(row = {}) { + if (!row || typeof row !== 'object') return null; + const htmlContent = firstNonEmptyString([row.html_content, row.html, row.content_html]); + const textContent = firstNonEmptyString([row.content, row.text, row.preview]); + const code = firstNonEmptyString([row.verification_code, row.verificationCode, row.code]); + const bodyPreview = [code, textContent || stripHtmlTags(htmlContent)].filter(Boolean).join(' '); + return { + id: firstNonEmptyString([row.id, row.emailId, row.mailId]), + address: normalizeFreemailAddress(firstNonEmptyString([ + row.to_addrs, + row.mailbox, + row.address, + ])), + subject: firstNonEmptyString([row.subject, row.title]), + from: { + emailAddress: { + address: firstNonEmptyString([row.sender, row.from, row.mailFrom]), + }, + }, + bodyPreview, + raw: htmlContent || textContent || code, + receivedDateTime: normalizeFreemailDate(firstNonEmptyString([ + row.received_at, + row.receivedDateTime, + row.created_at, + row.date, + ])), + }; + } + + function normalizeFreemailMessages(payload) { + return getFreemailRows(payload).map((row) => normalizeFreemailMessage(row)).filter(Boolean); + } + + function getFreemailAddressFromResponse(payload = {}) { + return firstNonEmptyString([ + payload.email, + payload.address, + payload.data?.email, + payload.data?.address, + ]); + } + + return { + DEFAULT_MAIL_PAGE_SIZE, + buildFreemailHeaders, + getFreemailAddressFromResponse, + joinFreemailUrl, + normalizeFreemailAddress, + normalizeFreemailBaseUrl, + normalizeFreemailDomain, + normalizeFreemailDomains, + normalizeFreemailMessages, + normalizeFreemailMessage, + }; +}); diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index 59fb73f..8648fab 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -552,6 +552,7 @@ @@ -578,6 +579,7 @@ + @@ -921,6 +923,40 @@ +