Files
FlowPilot/content/mail-163.js
T
unknown ee472675c1 feat: 163 mail — deduplicate codes + auto-delete after reading
- seenCodes Set tracks all codes extracted in this session
  - Step 7 won't pick up step 4's code even if email still exists
  - Across multi-run loops, old codes are skipped
- After extracting code, right-click mail item → click "删除邮件"
  - Keeps inbox clean for next poll cycle
  - Failure to delete is non-fatal (warn only)
2026-04-05 10:20:52 +08:00

243 lines
7.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// content/mail-163.js — Content script for 163 Mail (steps 4, 7)
// Injected on: mail.163.com
//
// DOM structure:
// Mail item: div[sign="letter"] with aria-label="你的 ChatGPT 代码为 479637 发件人 OpenAI ..."
// Sender: .nui-user (e.g., "OpenAI")
// Subject: span.da0 (e.g., "你的 ChatGPT 代码为 479637")
// Right-click menu: .nui-menu → .nui-menu-item with text "删除邮件"
const MAIL163_PREFIX = '[MultiPage:mail-163]';
const isTopFrame = window === window.top;
console.log(MAIL163_PREFIX, 'Content script loaded on', location.href, 'frame:', isTopFrame ? 'top' : 'child');
// Only operate in the top frame
if (!isTopFrame) {
console.log(MAIL163_PREFIX, 'Skipping child frame');
} else {
// Track codes we've already seen across polls to avoid duplicates
const seenCodes = new Set();
// ============================================================
// Message Handler (top frame only)
// ============================================================
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'POLL_EMAIL') {
handlePollEmail(message.step, message.payload).then(result => {
sendResponse(result);
}).catch(err => {
reportError(message.step, err.message);
sendResponse({ error: err.message });
});
return true;
}
});
// ============================================================
// Find mail items
// ============================================================
function findMailItems() {
return document.querySelectorAll('div[sign="letter"]');
}
function getCurrentMailIds() {
const ids = new Set();
findMailItems().forEach(item => {
const id = item.getAttribute('id') || '';
if (id) ids.add(id);
});
return ids;
}
// ============================================================
// Email Polling
// ============================================================
async function handlePollEmail(step, payload) {
const { senderFilters, subjectFilters, maxAttempts, intervalMs } = payload;
log(`Step ${step}: Starting email poll on 163 Mail (max ${maxAttempts} attempts)`);
// Click inbox in sidebar to ensure we're in inbox view
log(`Step ${step}: Waiting for sidebar...`);
try {
const inboxLink = await waitForElement('.nui-tree-item-text[title="收件箱"]', 5000);
inboxLink.click();
log(`Step ${step}: Clicked inbox`);
} catch {
log(`Step ${step}: Inbox link not found, proceeding...`, 'warn');
}
// Wait for mail list to appear
log(`Step ${step}: Waiting for mail list...`);
let items = [];
for (let i = 0; i < 20; i++) {
items = findMailItems();
if (items.length > 0) break;
await sleep(500);
}
if (items.length === 0) {
await refreshInbox();
await sleep(2000);
items = findMailItems();
}
if (items.length === 0) {
throw new Error('163 Mail list did not load. Make sure inbox is open.');
}
log(`Step ${step}: Mail list loaded, ${items.length} items`);
// Snapshot existing mail IDs
const existingMailIds = getCurrentMailIds();
log(`Step ${step}: Snapshotted ${existingMailIds.size} existing emails`);
const FALLBACK_AFTER = 3;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
log(`Polling 163 Mail... attempt ${attempt}/${maxAttempts}`);
if (attempt > 1) {
await refreshInbox();
await sleep(1000);
}
const allItems = findMailItems();
const useFallback = attempt > FALLBACK_AFTER;
for (const item of allItems) {
const id = item.getAttribute('id') || '';
if (!useFallback && existingMailIds.has(id)) continue;
const senderEl = item.querySelector('.nui-user');
const sender = senderEl ? senderEl.textContent.toLowerCase() : '';
const subjectEl = item.querySelector('span.da0');
const subject = subjectEl ? subjectEl.textContent : '';
const ariaLabel = (item.getAttribute('aria-label') || '').toLowerCase();
const senderMatch = senderFilters.some(f => sender.includes(f.toLowerCase()) || ariaLabel.includes(f.toLowerCase()));
const subjectMatch = subjectFilters.some(f => subject.toLowerCase().includes(f.toLowerCase()) || ariaLabel.includes(f.toLowerCase()));
if (senderMatch || subjectMatch) {
const code = extractVerificationCode(subject + ' ' + ariaLabel);
if (code && !seenCodes.has(code)) {
seenCodes.add(code);
const source = useFallback && existingMailIds.has(id) ? 'fallback' : 'new';
log(`Step ${step}: Code found: ${code} (${source}, subject: ${subject.slice(0, 40)})`, 'ok');
// Delete this email via right-click menu
await deleteEmail(item, step);
return { ok: true, code, emailTimestamp: Date.now(), mailId: id };
} else if (code && seenCodes.has(code)) {
log(`Step ${step}: Skipping already-seen code: ${code}`, 'info');
}
}
}
if (attempt === FALLBACK_AFTER + 1) {
log(`Step ${step}: No new emails after ${FALLBACK_AFTER} attempts, falling back to first match`, 'warn');
}
if (attempt < maxAttempts) {
await sleep(intervalMs);
}
}
throw new Error(
`No new matching email found on 163 Mail after ${(maxAttempts * intervalMs / 1000).toFixed(0)}s. ` +
'Check inbox manually.'
);
}
// ============================================================
// Delete Email via Right-Click Menu
// ============================================================
async function deleteEmail(item, step) {
try {
log(`Step ${step}: Deleting email...`);
// Right-click on the mail item to trigger context menu
item.dispatchEvent(new MouseEvent('contextmenu', {
bubbles: true, cancelable: true, button: 2,
clientX: item.getBoundingClientRect().x + 100,
clientY: item.getBoundingClientRect().y + 10,
}));
await sleep(500);
// Find the context menu and click "删除邮件"
const menuItems = document.querySelectorAll('.nui-menu-item .nui-menu-item-text');
for (const menuItem of menuItems) {
if (menuItem.textContent.trim() === '删除邮件') {
menuItem.closest('.nui-menu-item').click();
log(`Step ${step}: Email deleted`, 'ok');
await sleep(500);
return;
}
}
log(`Step ${step}: Could not find "删除邮件" in context menu`, 'warn');
} catch (err) {
log(`Step ${step}: Failed to delete email: ${err.message}`, 'warn');
}
}
// ============================================================
// Inbox Refresh
// ============================================================
async function refreshInbox() {
// Try toolbar "刷 新" button
const toolbarBtns = document.querySelectorAll('.nui-btn .nui-btn-text');
for (const btn of toolbarBtns) {
if (btn.textContent.replace(/\s/g, '') === '刷新') {
btn.closest('.nui-btn').click();
console.log(MAIL163_PREFIX, 'Clicked "刷新" button');
await sleep(800);
return;
}
}
// Fallback: click sidebar "收 信"
const shouXinBtns = document.querySelectorAll('.ra0');
for (const btn of shouXinBtns) {
if (btn.textContent.replace(/\s/g, '').includes('收信')) {
btn.click();
console.log(MAIL163_PREFIX, 'Clicked "收信" button');
await sleep(800);
return;
}
}
console.log(MAIL163_PREFIX, 'Could not find refresh button');
}
// ============================================================
// Verification Code Extraction
// ============================================================
function extractVerificationCode(text) {
const matchCn = text.match(/(?:代码为|验证码[^0-9]*?)[\s:]*(\d{6})/);
if (matchCn) return matchCn[1];
const matchEn = text.match(/code[:\s]+is[:\s]+(\d{6})|code[:\s]+(\d{6})/i);
if (matchEn) return matchEn[1] || matchEn[2];
const match6 = text.match(/\b(\d{6})\b/);
if (match6) return match6[1];
return null;
}
} // end of isTopFrame else block