Files

532 lines
16 KiB
JavaScript

// content/cloudflare-temp-email.js — Content script for Cloudflare Temp Email admin page
const CLOUDFLARE_TEMP_EMAIL_PREFIX = '[MultiPage:cloudflare-temp-email]';
const isTopFrame = window === window.top;
const {
combineDistinctTextParts = (parts = []) => parts
.map((part) => String(part || '').replace(/\s+/g, ' ').trim())
.filter(Boolean)
.filter((part, index, values) => values.indexOf(part) === index)
.join(' '),
extractVerificationCode = () => null,
generateReadableLocalPart = () => `mp${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`.slice(0, 18),
normalizeDomainSuffix = (value) => String(value || '').trim().replace(/^@+/, '').toLowerCase(),
parseCloudflareMailboxCredential = () => null,
pickRandomSuffix = (options = []) => normalizeDomainSuffix(options[0] || ''),
selectVerificationMessage = () => null,
} = globalThis.MultiPageCloudflareTempEmail || {};
console.log(CLOUDFLARE_TEMP_EMAIL_PREFIX, 'Content script loaded on', location.href, 'frame:', isTopFrame ? 'top' : 'child');
if (!isTopFrame) {
console.log(CLOUDFLARE_TEMP_EMAIL_PREFIX, 'Skipping child frame');
} else {
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type !== 'CREATE_CLOUDFLARE_TEMP_EMAIL' && message.type !== 'POLL_EMAIL') {
return;
}
resetStopState();
const handler = message.type === 'CREATE_CLOUDFLARE_TEMP_EMAIL'
? createCloudflareTempEmail
: pollCloudflareTempEmail;
handler(message.step, message.payload || {}).then((result) => {
sendResponse(result);
}).catch((err) => {
if (isStopError(err)) {
if (message.step) {
log(`Step ${message.step}: Stopped by user.`, 'warn');
} else {
log('Cloudflare Temp Email: Stopped by user.', 'warn');
}
sendResponse({ stopped: true, error: err.message });
return;
}
if (message.step) {
reportError(message.step, err.message);
}
sendResponse({ error: err.message });
});
return true;
});
function getElementText(el) {
return combineDistinctTextParts([
el?.innerText,
el?.textContent,
el?.getAttribute?.('aria-label'),
el?.getAttribute?.('title'),
el?.value,
]);
}
function normalizeText(value) {
return String(value || '').replace(/\s+/g, ' ').trim();
}
function normalizeEmail(value) {
return normalizeText(value).toLowerCase();
}
function isVisible(el) {
if (!el) return false;
if (el.hidden) return false;
const style = window.getComputedStyle(el);
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
return false;
}
return Boolean(el.offsetWidth || el.offsetHeight || el.getClientRects().length);
}
function findVisibleButton(pattern) {
return Array.from(document.querySelectorAll('button'))
.filter(isVisible)
.find((button) => pattern.test(normalizeText(getElementText(button))));
}
function findVisibleTab(pattern, occurrence = 'first') {
const tabs = Array.from(document.querySelectorAll('.n-tabs-tab'))
.filter(isVisible)
.filter((tab) => pattern.test(normalizeText(getElementText(tab))));
return occurrence === 'last' ? tabs[tabs.length - 1] || null : tabs[0] || null;
}
async function waitForCondition(predicate, timeout, message) {
const startedAt = Date.now();
while (Date.now() - startedAt < timeout) {
throwIfStopped();
const value = predicate();
if (value) {
return value;
}
await sleep(150);
}
throw new Error(message);
}
async function clickTab(pattern, occurrence = 'first', timeout = 10000) {
await waitForCondition(
() => findVisibleTab(pattern, occurrence),
timeout,
`Timed out waiting for tab ${pattern}`
);
const tab = findVisibleTab(pattern, occurrence);
if (!tab) {
throw new Error(`Could not find tab ${pattern}`);
}
await humanPause(120, 260);
simulateClick(tab);
await sleep(400);
return tab;
}
function getCreateAddressInput() {
return Array.from(document.querySelectorAll('input[placeholder="请输入"]')).find(isVisible) || null;
}
function getPrefixSwitch() {
return Array.from(document.querySelectorAll('[role="switch"], .n-switch')).find(isVisible) || null;
}
function getCreateInputGroup() {
return getCreateAddressInput()?.closest('.n-input-group') || null;
}
function getCreateDomainSelect() {
const group = getCreateInputGroup();
return Array.from(group?.querySelectorAll('.n-base-selection') || []).find(isVisible)
|| Array.from(group?.querySelectorAll('.n-select') || []).find(isVisible)
|| null;
}
function getCurrentDomain() {
const groupText = normalizeText(getCreateInputGroup()?.textContent || '');
const match = groupText.match(/@([a-z0-9.-]+\.[a-z]{2,})/i);
return normalizeDomainSuffix(match ? match[1] : '');
}
function getVisibleDomainOptionEntries() {
const seen = new Set();
const entries = [];
for (const option of Array.from(document.querySelectorAll('.n-base-select-option, [role="option"]'))) {
if (!isVisible(option)) {
continue;
}
const value = normalizeDomainSuffix(getElementText(option));
if (!value || seen.has(value)) {
continue;
}
seen.add(value);
entries.push({ option, value });
}
return entries;
}
async function ensureCreateAccountPage() {
await clickTab(/^账号$/, 'first');
await clickTab(/^创建账号$/);
await waitForCondition(
() => getCreateAddressInput(),
10000,
'Cloudflare Temp Email create form did not load.'
);
}
async function ensureMailPage() {
await clickTab(/^邮件$/, 'first');
await clickTab(/^邮件$/, 'last');
await waitForCondition(
() => document.querySelector('input[placeholder="留空查询所有地址"]'),
10000,
'Cloudflare Temp Email mail page did not load.'
);
}
async function ensurePrefixDisabled() {
const prefixSwitch = getPrefixSwitch();
if (!prefixSwitch) {
log('Cloudflare Temp Email: Prefix switch not present, assuming prefix is already disabled', 'info');
return;
}
if (prefixSwitch.getAttribute('aria-checked') === 'true') {
await humanPause(120, 280);
simulateClick(prefixSwitch);
await waitForCondition(
() => prefixSwitch.getAttribute('aria-checked') === 'false',
5000,
'Cloudflare Temp Email prefix switch did not turn off.'
);
log('Cloudflare Temp Email: Prefix disabled', 'ok');
}
}
async function selectRandomDomainSuffix() {
const domainSelect = await waitForCondition(
() => getCreateDomainSelect(),
5000,
'Could not find the Cloudflare Temp Email suffix selector.'
);
await humanPause(120, 260);
simulateClick(domainSelect);
await sleep(300);
const optionEntries = await waitForCondition(
() => {
const entries = getVisibleDomainOptionEntries();
return entries.length > 0 ? entries : null;
},
5000,
'Could not find any available Cloudflare Temp Email suffix options.'
);
const suffix = pickRandomSuffix(optionEntries.map((entry) => entry.value));
const selectedEntry = optionEntries.find((entry) => entry.value === suffix);
if (!suffix || !selectedEntry?.option) {
throw new Error('Could not choose a Cloudflare Temp Email suffix from the available options.');
}
await humanPause(120, 260);
simulateClick(selectedEntry.option);
await waitForCondition(
() => getCurrentDomain() === suffix,
5000,
`Cloudflare Temp Email suffix did not switch to ${suffix}.`
);
log(`Cloudflare Temp Email: Selected suffix ${suffix}`, 'info');
return suffix;
}
function generateLocalPart() {
return generateReadableLocalPart();
}
function findCredentialDialog() {
return Array.from(
document.querySelectorAll('[role="dialog"], .n-dialog, .n-modal, .n-base-modal, .n-card')
).find((el) => isVisible(el) && /邮箱地址凭证/.test(getElementText(el)));
}
function extractCredentialToken(root) {
if (!root) return '';
const candidates = [
getElementText(root),
...Array.from(root.querySelectorAll('textarea, input, pre, code')).map((el) => el.value || el.textContent || ''),
];
for (const candidate of candidates) {
const match = String(candidate || '').match(/[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/);
if (match) {
return match[0];
}
}
return '';
}
async function waitForCredentialDetails(timeout = 15000) {
return waitForCondition(() => {
const dialog = findCredentialDialog();
const token = extractCredentialToken(dialog);
const credential = parseCloudflareMailboxCredential(token);
if (!credential?.email) {
return null;
}
return credential;
}, timeout, 'Timed out waiting for Cloudflare Temp Email credential dialog.');
}
async function dismissCredentialDialog() {
const dialog = findCredentialDialog();
if (!dialog) return;
const closeButton = Array.from(dialog.querySelectorAll('button, .n-base-close'))
.find((el) => isVisible(el) && (/关闭|确定|取消/.test(getElementText(el)) || el.classList?.contains('n-base-close')));
if (closeButton) {
simulateClick(closeButton);
await sleep(300);
return;
}
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
await sleep(300);
}
async function createCloudflareTempEmail(step, payload = {}) {
const { generateNew = true } = payload;
await ensureCreateAccountPage();
await ensurePrefixDisabled();
await selectRandomDomainSuffix();
const input = getCreateAddressInput();
if (!input) {
throw new Error('Could not find the Cloudflare Temp Email address input.');
}
const createButton = findVisibleButton(/^创建新邮箱$/);
if (!createButton) {
throw new Error('Could not find the "创建新邮箱" button.');
}
for (let attempt = 1; attempt <= 3; attempt++) {
const localPart = generateLocalPart();
fillInput(input, localPart);
await humanPause(120, 260);
simulateClick(createButton);
log(`Cloudflare Temp Email: Creating mailbox attempt ${attempt}`, 'info');
try {
const credential = await waitForCredentialDetails(10000);
await dismissCredentialDialog();
return {
...credential,
domain: credential.domain || getCurrentDomain(),
generated: Boolean(generateNew),
};
} catch (err) {
if (attempt === 3) {
throw err;
}
log(`Cloudflare Temp Email: Mailbox attempt ${attempt} did not complete, retrying`, 'warn');
await dismissCredentialDialog().catch(() => {});
await sleep(500);
}
}
throw new Error('Cloudflare Temp Email mailbox creation did not succeed.');
}
function getMessageRows() {
return Array.from(document.querySelectorAll('.n-thing')).filter(isVisible);
}
function parseMessageRow(row) {
const rowText = row?.innerText || getElementText(row);
const subject = normalizeText(
row.querySelector('.n-thing-header__title')?.textContent
|| row.querySelector('h3, h4, h2')?.textContent
|| ''
);
const messageId = rowText.match(/ID:\s*([^\s]+)/i)?.[1] || null;
const timestampText = rowText.match(/\d{4}\/\d{1,2}\/\d{1,2}\s+\d{1,2}:\d{2}:\d{2}/)?.[0] || '';
const sender = normalizeText(rowText.match(/FROM:\s*([^\n]+)/i)?.[1] || '');
const matchedEmail = normalizeEmail(rowText.match(/TO:\s*([^\n]+)/i)?.[1] || '');
return {
combinedText: normalizeText(rowText),
emailTimestamp: null,
matchedEmail,
messageId,
row,
sender,
subject,
timestampText,
};
}
function findMessageDetailRoot() {
const deleteButton = Array.from(document.querySelectorAll('button'))
.find((button) => isVisible(button) && /^删除$/.test(normalizeText(getElementText(button))));
let current = deleteButton?.parentElement || null;
while (current && current !== document.body) {
const text = getElementText(current);
if (/FROM:/i.test(text) && /TO:/i.test(text)) {
return current;
}
current = current.parentElement;
}
return null;
}
async function openMessageRow(row, subject) {
await humanPause(80, 180);
simulateClick(row);
await waitForCondition(() => {
const detailRoot = findMessageDetailRoot();
const detailText = normalizeText(getElementText(detailRoot));
if (!detailText) return null;
if (!subject || detailText.includes(subject)) {
return detailRoot;
}
return null;
}, 4000, `Timed out opening message ${subject || ''}`.trim());
}
function buildMessageDetailText() {
return normalizeText(getElementText(findMessageDetailRoot()));
}
async function collectMessagesForTarget(targetEmail) {
const normalizedTargetEmail = normalizeEmail(targetEmail);
const rows = getMessageRows();
const messages = [];
for (const row of rows) {
const message = parseMessageRow(row);
if (!message.matchedEmail || message.matchedEmail !== normalizedTargetEmail) {
continue;
}
if (!extractVerificationCode(`${message.subject} ${message.combinedText}`)) {
await openMessageRow(row, message.subject);
message.combinedText = `${message.combinedText} ${buildMessageDetailText()}`.trim();
}
messages.push(message);
}
return messages;
}
async function runMailQuery(targetEmail) {
const queryInput = document.querySelector('input[placeholder="留空查询所有地址"]');
if (!queryInput) {
throw new Error('Could not find the admin mail query input.');
}
const queryButton = findVisibleButton(/^查询$/);
if (!queryButton) {
throw new Error('Could not find the admin mail query button.');
}
fillInput(queryInput, targetEmail);
await humanPause(80, 180);
simulateClick(queryButton);
await sleep(800);
}
async function refreshMailList() {
const refreshButton = findVisibleButton(/^刷新$/);
if (!refreshButton) {
throw new Error('Could not find the admin mail refresh button.');
}
simulateClick(refreshButton);
await sleep(1000);
}
async function pollCloudflareTempEmail(step, payload = {}) {
const {
filterAfterTimestamp = 0,
intervalMs = 3000,
maxAttempts = 20,
senderFilters = [],
subjectFilters = [],
targetEmail = '',
} = payload;
if (!targetEmail) {
throw new Error('No target email provided for Cloudflare Temp Email polling.');
}
await ensureMailPage();
await runMailQuery(targetEmail);
log(`Step ${step}: Starting Cloudflare Temp Email poll for ${targetEmail}`, 'info');
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
log(`Step ${step}: Polling Cloudflare Temp Email... attempt ${attempt}/${maxAttempts}`, 'info');
await refreshMailList();
const messages = await collectMessagesForTarget(targetEmail);
const match = selectVerificationMessage(messages, {
filterAfterTimestamp,
senderFilters,
subjectFilters,
targetEmail,
});
if (match?.code) {
log(
`Step ${step}: Code found: ${match.code} (subject: ${(match.subject || '').slice(0, 60)})`,
'ok'
);
return {
ok: true,
code: match.code,
emailTimestamp: match.emailTimestamp,
matchedEmail: match.matchedEmail,
messageId: match.messageId,
subject: match.subject,
};
}
if (attempt < maxAttempts) {
await sleep(intervalMs);
}
}
const newerSuffix = Number(filterAfterTimestamp) > 0
? ' newer than the previous verification message'
: '';
throw new Error(
`No matching verification email${newerSuffix} was found for ${targetEmail} after ${(maxAttempts * intervalMs / 1000).toFixed(0)}s.`
);
}
}