Files

266 lines
8.2 KiB
JavaScript

// content/relay-firefox.js — Content script for Firefox Relay profile page
console.log('[MultiPage:relay-firefox] Content script loaded on', location.href);
const {
getNextRelayMaskLabel = (labels = []) => `t${labels.length + 1}`,
} = globalThis.MultiPageEmailProvider || {};
const LABEL_INPUT_SELECTOR = 'input[placeholder="Add account name"], input[aria-label="Edit the label for this mask"]';
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type !== 'CREATE_RELAY_MASK' && message.type !== 'DELETE_RELAY_MASK') return;
resetStopState();
const handler = message.type === 'CREATE_RELAY_MASK'
? createRelayMask
: deleteRelayMask;
handler(message.payload || {}).then(result => {
sendResponse(result);
}).catch(err => {
if (isStopError(err)) {
log('Relay: Stopped by user.', 'warn');
sendResponse({ stopped: true, error: err.message });
return;
}
sendResponse({ error: err.message });
});
return true;
});
function getElementText(el) {
return [
el?.innerText,
el?.textContent,
el?.getAttribute?.('aria-label'),
el?.getAttribute?.('title'),
el?.getAttribute?.('description'),
].filter(Boolean).join(' ');
}
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 extractMozmail(text) {
const match = String(text || '').match(/[A-Z0-9._%+-]+@mozmail\.com/i);
return match ? match[0].toLowerCase() : '';
}
function getMaskButtons(root = document) {
return Array.from(root.querySelectorAll('button')).filter((button) => extractMozmail(getElementText(button)));
}
function getMaskEmails(root = document) {
return Array.from(new Set(
getMaskButtons(root)
.map((button) => extractMozmail(getElementText(button)))
.filter(Boolean)
));
}
function getVisibleLabelInputs(root = document) {
return Array.from(root.querySelectorAll(LABEL_INPUT_SELECTOR)).filter(isVisible);
}
function getExistingLabels() {
return Array.from(document.querySelectorAll(LABEL_INPUT_SELECTOR))
.map((input) => input.value.trim())
.filter(Boolean);
}
function findGenerateButton() {
return document.querySelector('button[title="Generate new mask"]')
|| Array.from(document.querySelectorAll('button')).find((button) => /generate new mask/i.test(getElementText(button)));
}
function findDeleteButton(root) {
return Array.from(root.querySelectorAll('button')).find((button) => /^delete$/i.test(getElementText(button).trim()));
}
function getMaskButtonsIn(root) {
return Array.from(root.querySelectorAll('button')).filter((button) => extractMozmail(getElementText(button)));
}
function findMaskContainerForButton(button) {
let current = button?.parentElement || null;
while (current && current !== document.body) {
const maskButtons = getMaskButtonsIn(current);
if (maskButtons.length === 1 && (current.querySelector(LABEL_INPUT_SELECTOR) || findDeleteButton(current))) {
return current;
}
current = current.parentElement;
}
return button?.closest('li') || button?.parentElement || null;
}
function findMaskRowByEmail(email) {
const normalizedEmail = String(email || '').toLowerCase();
const button = getMaskButtons().find((candidate) => extractMozmail(getElementText(candidate)) === normalizedEmail);
if (!button) return null;
return findMaskContainerForButton(button);
}
async function waitForNewMaskEmail(previousEmails = new Set(), timeout = 15000) {
const startedAt = Date.now();
while (Date.now() - startedAt < timeout) {
throwIfStopped();
const currentEmails = getMaskEmails();
const nextEmail = currentEmails.find((email) => !previousEmails.has(email));
if (nextEmail) {
return nextEmail;
}
await sleep(150);
}
throw new Error('Timed out waiting for a new Relay mask to appear.');
}
async function waitForMaskRow(email, timeout = 10000) {
const startedAt = Date.now();
while (Date.now() - startedAt < timeout) {
throwIfStopped();
const row = findMaskRowByEmail(email);
if (row) {
return row;
}
await sleep(150);
}
throw new Error(`Timed out waiting for Relay mask row: ${email}`);
}
async function assignRelayLabel(maskRow) {
const labelInput = Array.from(maskRow.querySelectorAll(LABEL_INPUT_SELECTOR)).find(isVisible)
|| maskRow.querySelector(LABEL_INPUT_SELECTOR);
if (!labelInput) {
throw new Error('Could not find Relay label input for the new mask.');
}
const currentValue = labelInput.value.trim();
if (currentValue) {
return currentValue;
}
const nextLabel = getNextRelayMaskLabel(getExistingLabels());
await humanPause(200, 450);
fillInput(labelInput, nextLabel);
labelInput.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
labelInput.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', bubbles: true }));
labelInput.blur();
for (let i = 0; i < 20; i++) {
throwIfStopped();
const labels = getExistingLabels();
if (labels.includes(nextLabel) || labelInput.value.trim() === nextLabel) {
log(`Relay: Assigned label ${nextLabel}`, 'ok');
return nextLabel;
}
await sleep(150);
}
throw new Error(`Relay label ${nextLabel} was not saved.`);
}
async function createRelayMask(payload = {}) {
const { generateNew = true } = payload;
log(`Relay: ${generateNew ? 'Creating' : 'Reading'} mask...`);
await waitForElement(LABEL_INPUT_SELECTOR + ', button[title="Generate new mask"]', 20000);
const previousEmails = new Set(getMaskEmails());
if (!generateNew && previousEmails.size > 0) {
const email = Array.from(previousEmails)[0];
return { email, label: null, generated: false };
}
const generatorButton = findGenerateButton();
if (!generatorButton) {
throw new Error('Could not find "Generate new mask" button on Firefox Relay.');
}
await humanPause(500, 1200);
simulateClick(generatorButton);
log('Relay: Clicked "Generate new mask"');
const email = await waitForNewMaskEmail(previousEmails);
const maskRow = await waitForMaskRow(email);
const label = await assignRelayLabel(maskRow);
log(`Relay: Ready mask ${email}`, 'ok');
return { email, label, generated: true };
}
function findVisibleDialogDeleteButton() {
const dialogButtons = Array.from(document.querySelectorAll('[role="dialog"] button, dialog button, [aria-modal="true"] button'));
return dialogButtons.find((button) => isVisible(button) && /delete|confirm|remove/i.test(getElementText(button)));
}
async function waitForMaskRemoval(email, timeout = 15000) {
const startedAt = Date.now();
const normalizedEmail = String(email || '').toLowerCase();
while (Date.now() - startedAt < timeout) {
throwIfStopped();
const exists = getMaskEmails().includes(normalizedEmail);
if (!exists) {
return;
}
await sleep(200);
}
throw new Error(`Timed out waiting for Relay mask deletion: ${email}`);
}
async function deleteRelayMask(payload = {}) {
const email = String(payload.email || '').trim().toLowerCase();
if (!email) {
throw new Error('No Relay mask email provided for deletion.');
}
log(`Relay: Deleting ${email}...`);
await waitForElement(LABEL_INPUT_SELECTOR + ', button[title="Generate new mask"]', 20000);
const maskRow = await waitForMaskRow(email);
const detailsButton = Array.from(maskRow.querySelectorAll('button')).find((button) => /show mask details/i.test(getElementText(button)));
if (detailsButton && isVisible(detailsButton)) {
await humanPause(150, 300);
simulateClick(detailsButton);
await sleep(250);
}
const deleteButton = findDeleteButton(maskRow);
if (!deleteButton) {
throw new Error(`Could not find Delete button for Relay mask ${email}.`);
}
await humanPause(200, 400);
simulateClick(deleteButton);
await sleep(300);
const confirmButton = findVisibleDialogDeleteButton();
if (confirmButton) {
await humanPause(150, 300);
simulateClick(confirmButton);
}
await waitForMaskRemoval(email);
log(`Relay: Deleted ${email}`, 'ok');
return { deleted: true, email };
}