phplife-mail.js 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985
  1. // content/phplife-mail.js — Content script for A4Sky mailbox on mail.phplife.net
  2. // Injected dynamically on: mail.phplife.net
  3. const PHPLIFE_MAIL_PREFIX = '[MultiPage:mail-phplife]';
  4. const isTopFrame = window === window.top;
  5. console.log(PHPLIFE_MAIL_PREFIX, 'Content script loaded on', location.href, 'frame:', isTopFrame ? 'top' : 'child');
  6. if (!isTopFrame) {
  7. console.log(PHPLIFE_MAIL_PREFIX, 'Skipping child frame');
  8. } else {
  9. let seenCodes = new Set();
  10. let waitingLoginLogged = false;
  11. async function loadSeenCodes() {
  12. try {
  13. const data = await chrome.storage.session.get('seenPhplifeCodes');
  14. if (Array.isArray(data.seenPhplifeCodes)) {
  15. seenCodes = new Set(data.seenPhplifeCodes.filter(Boolean));
  16. }
  17. } catch (err) {
  18. console.warn(PHPLIFE_MAIL_PREFIX, 'Could not load seen codes:', err?.message || err);
  19. }
  20. }
  21. loadSeenCodes();
  22. async function persistSeenCodes() {
  23. try {
  24. await chrome.storage.session.set({ seenPhplifeCodes: [...seenCodes] });
  25. } catch (err) {
  26. console.warn(PHPLIFE_MAIL_PREFIX, 'Could not persist seen codes:', err?.message || err);
  27. }
  28. }
  29. chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  30. if (message.type === 'POLL_EMAIL') {
  31. resetStopState();
  32. handlePollEmail(message.step, message.payload).then((result) => {
  33. sendResponse(result);
  34. }).catch((err) => {
  35. if (isStopError(err)) {
  36. log(`步骤 ${message.step}:已被用户停止。`, 'warn');
  37. sendResponse({ stopped: true, error: err.message });
  38. return;
  39. }
  40. log(`步骤 ${message.step}:A4Sky 邮箱轮询失败:${err.message}`, 'warn');
  41. sendResponse({ error: err.message });
  42. });
  43. return true;
  44. }
  45. });
  46. function normalizeText(value) {
  47. return String(value || '').replace(/\s+/g, ' ').trim();
  48. }
  49. function sleep(ms) {
  50. return new Promise((resolve, reject) => {
  51. if (flowStopped) {
  52. reject(new Error(STOP_ERROR_MESSAGE));
  53. return;
  54. }
  55. setTimeout(() => {
  56. if (flowStopped) {
  57. reject(new Error(STOP_ERROR_MESSAGE));
  58. return;
  59. }
  60. resolve();
  61. }, ms);
  62. });
  63. }
  64. function isVisibleElement(element) {
  65. if (!element) return false;
  66. const style = window.getComputedStyle(element);
  67. if (style.display === 'none' || style.visibility === 'hidden') return false;
  68. const rect = element.getBoundingClientRect();
  69. return rect.width > 0 && rect.height > 0;
  70. }
  71. function getRcmailEnv() {
  72. try {
  73. return window.rcmail?.env || null;
  74. } catch {
  75. return null;
  76. }
  77. }
  78. function isLoginPageLikely() {
  79. const url = location.href.toLowerCase();
  80. if (/[_-]task=login|[?&]_action=login\b|\/login\b/.test(url)) {
  81. return true;
  82. }
  83. const passwordInput = document.querySelector('input[type="password"]');
  84. const loginForm = document.querySelector('form[action*="login"], form[name*="login"], #login-form, .login-form');
  85. const loginButton = Array.from(document.querySelectorAll('button, input[type="submit"], a')).find((element) => {
  86. const text = normalizeText(
  87. element?.textContent
  88. || element?.value
  89. || element?.getAttribute?.('title')
  90. || element?.getAttribute?.('aria-label')
  91. || ''
  92. );
  93. return /登录|登入|sign in|log in/i.test(text);
  94. });
  95. const mailboxList = document.querySelector('#mailboxlist');
  96. const messageList = document.querySelector('#messagelist');
  97. const hasMailUi = mailboxList || messageList || getRcmailEnv()?.task === 'mail';
  98. if ((passwordInput || loginForm || loginButton) && !hasMailUi) {
  99. return true;
  100. }
  101. const pageText = normalizeText(document.body?.innerText || document.body?.textContent || '');
  102. return /(企业邮箱登录|请输入密码|登录邮箱|sign in)/i.test(pageText) && !hasMailUi;
  103. }
  104. async function waitUntilLoggedIn(step) {
  105. let loggedWaitMessage = false;
  106. while (isLoginPageLikely()) {
  107. throwIfStopped();
  108. if (!loggedWaitMessage && !waitingLoginLogged) {
  109. waitingLoginLogged = true;
  110. loggedWaitMessage = true;
  111. log(`步骤 ${step}:检测到 mail.phplife.net 未登录,正在等待你手动登录...`, 'warn');
  112. }
  113. await sleep(1000);
  114. }
  115. if (loggedWaitMessage || waitingLoginLogged) {
  116. log(`步骤 ${step}:已检测到 mail.phplife.net 登录完成,继续读取验证码...`, 'ok');
  117. }
  118. waitingLoginLogged = false;
  119. }
  120. function normalizeMinuteTimestamp(timestamp) {
  121. if (!Number.isFinite(timestamp) || timestamp <= 0) return 0;
  122. const date = new Date(timestamp);
  123. date.setSeconds(0, 0);
  124. return date.getTime();
  125. }
  126. function parseRoundcubeTimestamp(rawText) {
  127. const text = normalizeText(rawText);
  128. if (!text) return null;
  129. let match = text.match(/今天\s*(\d{1,2}):(\d{2})/);
  130. if (match) {
  131. const now = new Date();
  132. return new Date(now.getFullYear(), now.getMonth(), now.getDate(), Number(match[1]), Number(match[2]), 0, 0).getTime();
  133. }
  134. match = text.match(/昨天\s*(\d{1,2}):(\d{2})/);
  135. if (match) {
  136. const now = new Date();
  137. now.setDate(now.getDate() - 1);
  138. return new Date(now.getFullYear(), now.getMonth(), now.getDate(), Number(match[1]), Number(match[2]), 0, 0).getTime();
  139. }
  140. match = text.match(/(\d{4})[-/年](\d{1,2})[-/月](\d{1,2})日?\s*(\d{1,2}):(\d{2})/);
  141. if (match) {
  142. return new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]), Number(match[4]), Number(match[5]), 0, 0).getTime();
  143. }
  144. match = text.match(/(\d{1,2})[-/](\d{1,2})\s*(\d{1,2}):(\d{2})/);
  145. if (match) {
  146. const now = new Date();
  147. return new Date(now.getFullYear(), Number(match[1]) - 1, Number(match[2]), Number(match[3]), Number(match[4]), 0, 0).getTime();
  148. }
  149. match = text.match(/^(\d{1,2}):(\d{2})$/);
  150. if (match) {
  151. const now = new Date();
  152. return new Date(now.getFullYear(), now.getMonth(), now.getDate(), Number(match[1]), Number(match[2]), 0, 0).getTime();
  153. }
  154. const parsed = Date.parse(text);
  155. return Number.isFinite(parsed) ? parsed : null;
  156. }
  157. function extractVerificationCodes(text) {
  158. const source = String(text || '');
  159. const codes = [];
  160. const patterns = [
  161. /(?:验证码|代码)[^0-9]{0,24}(\d{6})/ig,
  162. /(?:chatgpt\s+log-?in\s+code|your\s+chatgpt\s+code\s+is|verification\s+code|temporary\s+verification\s+code|enter\s+this\s+code)[^0-9]{0,24}(\d{6})/ig,
  163. /\b(\d{6})\b/g,
  164. ];
  165. for (const pattern of patterns) {
  166. let match = null;
  167. while ((match = pattern.exec(source))) {
  168. if (match[1] && !codes.includes(match[1])) {
  169. codes.push(match[1]);
  170. }
  171. }
  172. if (codes.length) {
  173. return codes;
  174. }
  175. }
  176. return codes;
  177. }
  178. function extractEmails(text) {
  179. const matches = String(text || '').match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/ig) || [];
  180. return [...new Set(matches.map((item) => item.toLowerCase()))];
  181. }
  182. function collectPayloadStringFragments(value, bucket = []) {
  183. if (typeof value === 'string') {
  184. const normalized = value.trim();
  185. if (normalized) {
  186. bucket.push(normalized);
  187. }
  188. return bucket;
  189. }
  190. if (Array.isArray(value)) {
  191. value.forEach((item) => collectPayloadStringFragments(item, bucket));
  192. return bucket;
  193. }
  194. if (value && typeof value === 'object') {
  195. Object.values(value).forEach((item) => collectPayloadStringFragments(item, bucket));
  196. }
  197. return bucket;
  198. }
  199. function extractRemoteExecText(rawText = '') {
  200. try {
  201. const parsed = JSON.parse(String(rawText || ''));
  202. const execText = String(parsed?.exec || '').trim();
  203. return execText || String(rawText || '');
  204. } catch {
  205. return String(rawText || '');
  206. }
  207. }
  208. function readBalancedJsonObject(source = '', startIndex = 0) {
  209. if (String(source || '')[startIndex] !== '{') {
  210. return null;
  211. }
  212. let depth = 0;
  213. let inString = false;
  214. let escaped = false;
  215. for (let index = startIndex; index < source.length; index += 1) {
  216. const ch = source[index];
  217. if (escaped) {
  218. escaped = false;
  219. continue;
  220. }
  221. if (ch === '\\') {
  222. escaped = true;
  223. continue;
  224. }
  225. if (ch === '"') {
  226. inString = !inString;
  227. continue;
  228. }
  229. if (inString) {
  230. continue;
  231. }
  232. if (ch === '{') depth += 1;
  233. if (ch === '}') {
  234. depth -= 1;
  235. if (depth === 0) {
  236. return {
  237. text: source.slice(startIndex, index + 1),
  238. endIndex: index + 1,
  239. };
  240. }
  241. }
  242. }
  243. return null;
  244. }
  245. function parseRemoteMessageRowEntries(rawText = '') {
  246. const source = extractRemoteExecText(rawText);
  247. const entries = [];
  248. const marker = 'this.add_message_row(';
  249. let cursor = 0;
  250. while (cursor < source.length) {
  251. const start = source.indexOf(marker, cursor);
  252. if (start < 0) {
  253. break;
  254. }
  255. const argsStart = start + marker.length;
  256. const firstComma = source.indexOf(',', argsStart);
  257. if (firstComma < 0) {
  258. break;
  259. }
  260. const uid = String(source.slice(argsStart, firstComma).trim() || '');
  261. const rowObjectStart = source.indexOf('{', firstComma);
  262. if (rowObjectStart < 0) {
  263. break;
  264. }
  265. const rowObject = readBalancedJsonObject(source, rowObjectStart);
  266. if (!rowObject) {
  267. break;
  268. }
  269. const metaObjectStart = source.indexOf('{', rowObject.endIndex);
  270. const metaObject = metaObjectStart >= 0 ? readBalancedJsonObject(source, metaObjectStart) : null;
  271. try {
  272. const rowData = JSON.parse(rowObject.text);
  273. const metaData = metaObject ? JSON.parse(metaObject.text) : {};
  274. entries.push({ uid, rowData, metaData });
  275. } catch {
  276. // Ignore malformed row and continue scanning.
  277. }
  278. cursor = metaObject?.endIndex || rowObject.endIndex;
  279. }
  280. return entries;
  281. }
  282. function extractRemotePayloadFragments(rawText = '') {
  283. const fragments = [];
  284. const addFragment = (value) => {
  285. const normalized = String(value || '').trim();
  286. if (normalized) {
  287. fragments.push(normalized);
  288. }
  289. };
  290. let parsedJson = null;
  291. try {
  292. parsedJson = JSON.parse(rawText);
  293. } catch {
  294. parsedJson = null;
  295. }
  296. if (parsedJson) {
  297. collectPayloadStringFragments(parsedJson.exec || parsedJson, fragments);
  298. } else {
  299. addFragment(rawText);
  300. }
  301. const expanded = [];
  302. fragments.forEach((fragment) => {
  303. const rowMatches = String(fragment).match(/<tr\b[\s\S]*?<\/tr>/gi);
  304. if (rowMatches?.length) {
  305. expanded.push(...rowMatches);
  306. } else {
  307. expanded.push(fragment);
  308. }
  309. });
  310. return [...new Set(expanded.map((item) => String(item || '').trim()).filter(Boolean))];
  311. }
  312. function getTargetEmailMatchState(text, targetEmail) {
  313. const normalizedTarget = String(targetEmail || '').trim().toLowerCase();
  314. if (!normalizedTarget) {
  315. return { matches: true, hasExplicitEmail: false };
  316. }
  317. const normalizedText = String(text || '').toLowerCase();
  318. if (normalizedText.includes(normalizedTarget)) {
  319. return { matches: true, hasExplicitEmail: true };
  320. }
  321. const emails = extractEmails(text);
  322. if (!emails.length) {
  323. return { matches: false, hasExplicitEmail: false };
  324. }
  325. return {
  326. matches: emails.includes(normalizedTarget),
  327. hasExplicitEmail: true,
  328. };
  329. }
  330. function normalizeFragmentText(fragment = '') {
  331. const source = String(fragment || '');
  332. if (!source) return '';
  333. if (!/[<>]/.test(source)) {
  334. return normalizeText(source);
  335. }
  336. try {
  337. const parser = new DOMParser();
  338. const doc = parser.parseFromString(`<table><tbody>${source}</tbody></table>`, 'text/html');
  339. return normalizeText(doc.body?.textContent || doc.documentElement?.textContent || source);
  340. } catch {
  341. return normalizeText(source);
  342. }
  343. }
  344. function buildRemoteRowDetails(entry = {}) {
  345. const rowData = entry?.rowData || {};
  346. const metaData = entry?.metaData || {};
  347. const subject = normalizeText(rowData.subject || '');
  348. const from = normalizeFragmentText(rowData.fromto || '');
  349. const to = normalizeFragmentText(rowData.to || '');
  350. const dateText = normalizeText(rowData.date || '');
  351. const combinedText = normalizeText([subject, from, to, dateText].join(' '));
  352. return {
  353. uid: String(entry?.uid || ''),
  354. subject,
  355. from,
  356. to,
  357. dateText,
  358. ctype: String(metaData?.ctype || ''),
  359. emailTimestamp: parseRoundcubeTimestamp(dateText),
  360. codes: extractVerificationCodes(combinedText),
  361. combinedText,
  362. };
  363. }
  364. function findExactTargetRemoteRows(entries = [], targetEmail = '') {
  365. const normalizedTarget = String(targetEmail || '').trim().toLowerCase();
  366. if (!normalizedTarget) {
  367. return [];
  368. }
  369. return entries.filter((details) => {
  370. const targetState = getTargetEmailMatchState(details?.combinedText || '', normalizedTarget);
  371. return targetState.hasExplicitEmail && targetState.matches;
  372. });
  373. }
  374. function matchesMailFilters(text, senderFilters = [], subjectFilters = []) {
  375. const normalizedText = String(text || '').toLowerCase();
  376. const senderMatched = senderFilters.some((filter) => normalizedText.includes(String(filter || '').toLowerCase()));
  377. const subjectMatched = subjectFilters.some((filter) => normalizedText.includes(String(filter || '').toLowerCase()));
  378. return senderMatched || subjectMatched;
  379. }
  380. function getPreviewDocument() {
  381. const frame = document.getElementById('messagecontframe');
  382. if (!frame) return null;
  383. try {
  384. return frame.contentDocument || frame.contentWindow?.document || null;
  385. } catch {
  386. return null;
  387. }
  388. }
  389. function getMessageDocument() {
  390. if (document.querySelector('#messagebody, #messageheader, .headers-table')) {
  391. return document;
  392. }
  393. const previewDocument = getPreviewDocument();
  394. if (previewDocument?.querySelector?.('#messagebody, #messageheader, .headers-table')) {
  395. return previewDocument;
  396. }
  397. return null;
  398. }
  399. function getMessageDetailsFromDocument(doc = null) {
  400. const sourceDocument = doc || getMessageDocument();
  401. if (!sourceDocument) return null;
  402. const subject = normalizeText(sourceDocument.querySelector('h2.subject')?.textContent || '');
  403. const from = normalizeText(sourceDocument.querySelector('.headers-table .header.from')?.textContent || '');
  404. const to = normalizeText(sourceDocument.querySelector('.headers-table .header.to')?.textContent || '');
  405. const dateText = normalizeText(sourceDocument.querySelector('.headers-table .header.date')?.textContent || '');
  406. const bodyText = normalizeText(
  407. sourceDocument.querySelector('#messagebody')?.innerText
  408. || sourceDocument.querySelector('#messagebody')?.textContent
  409. || sourceDocument.body?.innerText
  410. || sourceDocument.body?.textContent
  411. || ''
  412. );
  413. const combinedText = normalizeText([subject, from, to, dateText, bodyText].join(' '));
  414. const codes = extractVerificationCodes(combinedText);
  415. return {
  416. subject,
  417. from,
  418. to,
  419. dateText,
  420. emailTimestamp: parseRoundcubeTimestamp(dateText),
  421. bodyText,
  422. combinedText,
  423. codes,
  424. };
  425. }
  426. function getMessageListRows() {
  427. return Array.from(document.querySelectorAll('#messagelist tbody tr')).filter(isVisibleElement);
  428. }
  429. function getRowText(row, selector) {
  430. const node = row?.querySelector(selector);
  431. return normalizeText(
  432. node?.getAttribute?.('title')
  433. || node?.getAttribute?.('aria-label')
  434. || node?.textContent
  435. || ''
  436. );
  437. }
  438. function getRowDetails(row) {
  439. const subject = getRowText(row, 'td.subject');
  440. const from = getRowText(row, 'td.fromto');
  441. const to = getRowText(row, 'td.to');
  442. const dateText = getRowText(row, 'td.date');
  443. const combinedText = normalizeText([subject, from, to, dateText, row?.textContent || ''].join(' '));
  444. return {
  445. row,
  446. subject,
  447. from,
  448. to,
  449. dateText,
  450. emailTimestamp: parseRoundcubeTimestamp(dateText),
  451. codes: extractVerificationCodes(combinedText),
  452. combinedText,
  453. };
  454. }
  455. function scoreRowCandidate(details, payload = {}) {
  456. const { senderFilters = [], subjectFilters = [], targetEmail = '' } = payload;
  457. let score = 0;
  458. const combinedText = details?.combinedText || '';
  459. if (matchesMailFilters(combinedText, senderFilters, subjectFilters)) score += 4;
  460. if (/openai|chatgpt|verification|verify|验证码/i.test(combinedText)) score += 3;
  461. const targetMatch = getTargetEmailMatchState(combinedText, targetEmail);
  462. if (targetMatch.matches) score += targetMatch.hasExplicitEmail ? 4 : 1;
  463. if (details?.emailTimestamp) score += 1;
  464. return score;
  465. }
  466. function scoreRemotePayloadCandidate(text = '', payload = {}) {
  467. const targetState = getTargetEmailMatchState(text, payload.targetEmail);
  468. let score = 0;
  469. if (targetState.matches) score += targetState.hasExplicitEmail ? 10 : 2;
  470. if (matchesMailFilters(text, payload.senderFilters, payload.subjectFilters)) score += 5;
  471. if (/openai|chatgpt|verification|verify|验证码|登录|login/i.test(text)) score += 4;
  472. if (shouldOpenRowForCodeDetection({ subject: text })) score += 2;
  473. return { score, targetState };
  474. }
  475. function buildRemoteMailApiUrl(action = 'list', options = {}) {
  476. const {
  477. targetEmail = '',
  478. unlockPrefix = 'loading',
  479. } = options;
  480. const requestAt = Date.now();
  481. const url = new URL(`/?_task=mail&_action=${encodeURIComponent(action)}`, location.origin);
  482. url.searchParams.set('_mbox', 'INBOX');
  483. url.searchParams.set('_remote', '1');
  484. url.searchParams.set('_unlock', `${unlockPrefix}${requestAt}`);
  485. url.searchParams.set('_', String(requestAt));
  486. if (action === 'list') {
  487. url.searchParams.set('_refresh', '1');
  488. }
  489. if (action === 'search' && targetEmail) {
  490. url.searchParams.set('_filter', 'ALL');
  491. url.searchParams.set('_interval', '');
  492. url.searchParams.set('_q', targetEmail);
  493. url.searchParams.set('_headers', 'to');
  494. url.searchParams.set('_scope', 'base');
  495. }
  496. return url;
  497. }
  498. async function requestRemoteMailApi(action, options = {}) {
  499. const url = buildRemoteMailApiUrl(action, options);
  500. const controller = new AbortController();
  501. const timeoutId = setTimeout(() => controller.abort(new Error('timeout')), 15000);
  502. try {
  503. const response = await fetch(url.toString(), {
  504. credentials: 'same-origin',
  505. headers: {
  506. Accept: 'application/json, text/javascript, */*; q=0.01',
  507. 'X-Requested-With': 'XMLHttpRequest',
  508. },
  509. cache: 'no-store',
  510. signal: controller.signal,
  511. });
  512. if (!response.ok) {
  513. throw new Error(`HTTP ${response.status}`);
  514. }
  515. return await response.text();
  516. } catch (error) {
  517. if (error?.name === 'AbortError') {
  518. throw new Error(`A4Sky ${action} 接口请求超时`);
  519. }
  520. throw new Error(`A4Sky ${action} 接口请求失败:${error?.message || error}`);
  521. } finally {
  522. clearTimeout(timeoutId);
  523. }
  524. }
  525. async function requestRemoteMessageList(step) {
  526. return requestRemoteMailApi('list', { unlockPrefix: 'loading' });
  527. }
  528. async function requestRemoteMessageSearch(step, targetEmail = '') {
  529. if (!String(targetEmail || '').trim()) {
  530. return '';
  531. }
  532. return requestRemoteMailApi('search', {
  533. targetEmail: String(targetEmail || '').trim(),
  534. unlockPrefix: 'loading',
  535. });
  536. }
  537. function findVerificationCodeFromRemotePayload(rawText, payload, excludedCodeSet = new Set(), filterAfterMinute = 0) {
  538. const parsedRows = parseRemoteMessageRowEntries(rawText)
  539. .map((entry) => buildRemoteRowDetails(entry));
  540. const exactTargetRows = findExactTargetRemoteRows(parsedRows, payload?.targetEmail);
  541. const candidateRows = exactTargetRows.length ? exactTargetRows : parsedRows;
  542. const rowCandidates = candidateRows
  543. .map((details) => {
  544. const match = matchesCurrentMessage(details, payload, excludedCodeSet, filterAfterMinute);
  545. return match.matched
  546. ? {
  547. code: match.code,
  548. emailTimestamp: match.emailTimestamp,
  549. score: scoreRowCandidate(details, payload),
  550. uid: Number(details.uid || 0) || 0,
  551. }
  552. : null;
  553. })
  554. .filter(Boolean)
  555. .sort((left, right) => {
  556. if (left.score !== right.score) return right.score - left.score;
  557. if (left.emailTimestamp !== right.emailTimestamp) return right.emailTimestamp - left.emailTimestamp;
  558. return right.uid - left.uid;
  559. });
  560. if (rowCandidates.length) {
  561. return rowCandidates[0];
  562. }
  563. if (exactTargetRows.length) {
  564. return null;
  565. }
  566. const fragments = extractRemotePayloadFragments(rawText);
  567. const candidates = [];
  568. fragments.forEach((fragment) => {
  569. const text = normalizeFragmentText(fragment);
  570. if (!text) return;
  571. const { score, targetState } = scoreRemotePayloadCandidate(text, payload);
  572. if (payload?.targetEmail && targetState.hasExplicitEmail && !targetState.matches) {
  573. return;
  574. }
  575. if (score <= 0) return;
  576. const codes = extractVerificationCodes(text);
  577. const code = selectCandidateCode(codes, excludedCodeSet);
  578. if (!code) return;
  579. candidates.push({
  580. code,
  581. score,
  582. text,
  583. targetMatched: targetState.matches,
  584. explicitTarget: targetState.hasExplicitEmail,
  585. });
  586. });
  587. candidates.sort((left, right) => {
  588. if (left.score !== right.score) return right.score - left.score;
  589. if (left.explicitTarget !== right.explicitTarget) return Number(right.explicitTarget) - Number(left.explicitTarget);
  590. if (left.targetMatched !== right.targetMatched) return Number(right.targetMatched) - Number(left.targetMatched);
  591. return left.text.length - right.text.length;
  592. });
  593. return candidates[0] || null;
  594. }
  595. async function tryReadRemoteMessageList(step, payload, excludedCodeSet) {
  596. try {
  597. const filterAfterMinute = normalizeMinuteTimestamp(Number(payload?.filterAfterTimestamp) || 0);
  598. const targetEmail = String(payload?.targetEmail || '').trim().toLowerCase();
  599. if (targetEmail) {
  600. const searchText = await requestRemoteMessageSearch(step, targetEmail);
  601. const searchResult = findVerificationCodeFromRemotePayload(
  602. searchText,
  603. payload,
  604. excludedCodeSet,
  605. filterAfterMinute
  606. );
  607. if (searchResult?.code) {
  608. log(`步骤 ${step}:已直接从 mail.phplife.net 搜索接口命中验证码邮件。`, 'ok');
  609. return {
  610. code: searchResult.code,
  611. emailTimestamp: Date.now(),
  612. };
  613. }
  614. }
  615. const rawText = await requestRemoteMessageList(step);
  616. const result = findVerificationCodeFromRemotePayload(
  617. rawText,
  618. payload,
  619. excludedCodeSet,
  620. filterAfterMinute
  621. );
  622. if (result?.code) {
  623. log(`步骤 ${step}:已直接从 mail.phplife.net 列表接口命中验证码邮件。`, 'ok');
  624. return {
  625. code: result.code,
  626. emailTimestamp: Date.now(),
  627. };
  628. }
  629. return null;
  630. } catch (error) {
  631. console.warn(PHPLIFE_MAIL_PREFIX, 'Remote list request failed:', error?.message || error);
  632. return null;
  633. }
  634. }
  635. function shouldOpenRowForCodeDetection(details = {}) {
  636. const subject = normalizeText(details?.subject || '');
  637. if (!subject) {
  638. return false;
  639. }
  640. return /your\s+temporary\s+chatgpt\s+login\s+code|(?:你(?:的)?|您的)?\s*临时\s*chatgpt\s*登录代?码/i.test(subject);
  641. }
  642. function getCurrentMessageUid() {
  643. const envUid = getRcmailEnv()?.uid;
  644. if (envUid) return String(envUid);
  645. const doc = getMessageDocument();
  646. const permaLink = doc?.querySelector?.('a[href*="_uid="]')?.getAttribute?.('href') || location.href;
  647. const match = String(permaLink || '').match(/[?&]_uid=(\d+)/);
  648. return match ? match[1] : '';
  649. }
  650. async function waitForPreviewLoaded(previousUid = '', timeoutMs = 15000) {
  651. const start = Date.now();
  652. while (Date.now() - start < timeoutMs) {
  653. throwIfStopped();
  654. const doc = getMessageDocument();
  655. const details = getMessageDetailsFromDocument(doc);
  656. const currentUid = getCurrentMessageUid();
  657. if (details?.combinedText && (!previousUid || !currentUid || currentUid !== previousUid)) {
  658. return details;
  659. }
  660. await sleep(250);
  661. }
  662. return getMessageDetailsFromDocument();
  663. }
  664. function openMessageRow(row) {
  665. if (!row) {
  666. return;
  667. }
  668. if (typeof row.click === 'function') {
  669. row.click();
  670. return;
  671. }
  672. row.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
  673. }
  674. function findRefreshButton() {
  675. const selectors = [
  676. '#rcmbtn105',
  677. 'a.button.checkmail',
  678. 'a[title*="检查新邮件"]',
  679. 'a[title*="刷新"]',
  680. ];
  681. for (const selector of selectors) {
  682. const node = document.querySelector(selector);
  683. if (node) return node;
  684. }
  685. return Array.from(document.querySelectorAll('a, button')).find((element) => /刷新|检查新邮件|check mail/i.test(normalizeText(element.textContent || element.getAttribute('title') || ''))) || null;
  686. }
  687. function findInboxLink() {
  688. return document.querySelector('#mailboxlist .mailbox.inbox a[rel="INBOX"], #mailboxlist a[rel="INBOX"]');
  689. }
  690. async function ensureInboxActive(options = {}) {
  691. const { forceRefresh = false } = options;
  692. const inboxLink = findInboxLink();
  693. if (!inboxLink) return;
  694. const inboxItem = inboxLink.closest('.mailbox');
  695. if (!forceRefresh && inboxItem?.classList?.contains('selected')) {
  696. return;
  697. }
  698. inboxLink.click();
  699. await sleep(800);
  700. }
  701. async function refreshMessageList() {
  702. await ensureInboxActive({ forceRefresh: true });
  703. const refreshButton = findRefreshButton();
  704. if (!refreshButton) return true;
  705. refreshButton.click();
  706. await sleep(1200);
  707. return true;
  708. }
  709. function selectCandidateCode(codes = [], excludedCodeSet = new Set()) {
  710. for (const code of codes) {
  711. if (!excludedCodeSet.has(code) && !seenCodes.has(code)) {
  712. return code;
  713. }
  714. }
  715. return null;
  716. }
  717. function matchesCurrentMessage(details, payload = {}, excludedCodeSet = new Set(), filterAfterMinute = 0) {
  718. if (!details?.combinedText) {
  719. return { matched: false };
  720. }
  721. const targetMatch = getTargetEmailMatchState(details.combinedText, payload.targetEmail);
  722. if (targetMatch.hasExplicitEmail && !targetMatch.matches) {
  723. return { matched: false };
  724. }
  725. if (!matchesMailFilters(details.combinedText, payload.senderFilters, payload.subjectFilters)) {
  726. return { matched: false };
  727. }
  728. const normalizedTimestamp = normalizeMinuteTimestamp(details.emailTimestamp || 0);
  729. if (filterAfterMinute && normalizedTimestamp && normalizedTimestamp < filterAfterMinute) {
  730. return { matched: false };
  731. }
  732. const code = selectCandidateCode(details.codes, excludedCodeSet);
  733. if (!code) {
  734. return { matched: false };
  735. }
  736. return {
  737. matched: true,
  738. code,
  739. emailTimestamp: details.emailTimestamp || Date.now(),
  740. };
  741. }
  742. async function tryReadCurrentMessage(payload, excludedCodeSet, filterAfterMinute) {
  743. const details = getMessageDetailsFromDocument();
  744. const result = matchesCurrentMessage(details, payload, excludedCodeSet, filterAfterMinute);
  745. return result.matched ? result : null;
  746. }
  747. async function tryOpenRowsAndRead(step, payload, excludedCodeSet, filterAfterMinute) {
  748. const rows = getMessageListRows()
  749. .map((row) => getRowDetails(row))
  750. .map((details) => ({ ...details, score: scoreRowCandidate(details, payload) }))
  751. .filter((details) => details.score > 0)
  752. .sort((left, right) => right.score - left.score);
  753. for (const details of rows.slice(0, 8)) {
  754. throwIfStopped();
  755. const rowMinuteTimestamp = normalizeMinuteTimestamp(details.emailTimestamp || 0);
  756. if (filterAfterMinute && rowMinuteTimestamp && rowMinuteTimestamp < filterAfterMinute) {
  757. continue;
  758. }
  759. const directResult = matchesCurrentMessage(details, payload, excludedCodeSet, filterAfterMinute);
  760. const shouldOpenDetail = shouldOpenRowForCodeDetection(details);
  761. if (directResult.matched && !shouldOpenDetail) {
  762. log(`步骤 ${step}:已直接从 mail.phplife.net 列表命中验证码邮件。`, 'ok');
  763. return directResult;
  764. }
  765. if (!shouldOpenDetail) {
  766. continue;
  767. }
  768. log(`步骤 ${step}:检测到临时登录验证码邮件标题,正在打开详情读取验证码...`, 'info');
  769. const previousUid = getCurrentMessageUid();
  770. openMessageRow(details.row);
  771. await sleep(500);
  772. const openedDetails = await waitForPreviewLoaded(previousUid);
  773. const detailResult = matchesCurrentMessage(openedDetails, payload, excludedCodeSet, filterAfterMinute);
  774. if (detailResult.matched) {
  775. log(`步骤 ${step}:已在 mail.phplife.net 邮件详情中命中验证码。`, 'ok');
  776. return detailResult;
  777. }
  778. if (directResult.matched) {
  779. log(`步骤 ${step}:邮件详情未提取到验证码,已回退使用列表中的匹配结果。`, 'warn');
  780. return directResult;
  781. }
  782. }
  783. return null;
  784. }
  785. async function handlePollEmail(step, payload) {
  786. const {
  787. maxAttempts = 5,
  788. intervalMs = 3000,
  789. excludeCodes = [],
  790. filterAfterTimestamp = 0,
  791. } = payload || {};
  792. const excludedCodeSet = new Set((excludeCodes || []).filter(Boolean));
  793. const filterAfterMinute = normalizeMinuteTimestamp(Number(filterAfterTimestamp) || 0);
  794. await waitUntilLoggedIn(step);
  795. await ensureInboxActive();
  796. log(`步骤 ${step}:开始轮询 A4Sky 邮箱(最多 ${maxAttempts} 次)`);
  797. if (filterAfterMinute) {
  798. log(`步骤 ${step}:仅尝试 ${new Date(filterAfterMinute).toLocaleString('zh-CN', { hour12: false })} 及之后时间的邮件。`);
  799. }
  800. let lastError = null;
  801. for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
  802. throwIfStopped();
  803. await waitUntilLoggedIn(step);
  804. log(`步骤 ${step}:正在检查 mail.phplife.net 邮件(${attempt}/${maxAttempts})...`);
  805. await refreshMessageList();
  806. const remoteListResult = await tryReadRemoteMessageList(step, payload, excludedCodeSet);
  807. if (remoteListResult?.code) {
  808. seenCodes.add(remoteListResult.code);
  809. await persistSeenCodes();
  810. return remoteListResult;
  811. }
  812. const currentMessageResult = await tryReadCurrentMessage(payload, excludedCodeSet, filterAfterMinute);
  813. if (currentMessageResult?.matched) {
  814. seenCodes.add(currentMessageResult.code);
  815. await persistSeenCodes();
  816. return {
  817. code: currentMessageResult.code,
  818. emailTimestamp: currentMessageResult.emailTimestamp,
  819. };
  820. }
  821. const openedRowResult = await tryOpenRowsAndRead(step, payload, excludedCodeSet, filterAfterMinute);
  822. if (openedRowResult?.matched) {
  823. seenCodes.add(openedRowResult.code);
  824. await persistSeenCodes();
  825. return {
  826. code: openedRowResult.code,
  827. emailTimestamp: openedRowResult.emailTimestamp,
  828. };
  829. }
  830. lastError = new Error(`步骤 ${step}:暂未在 A4Sky 邮箱中找到新的匹配验证码(${attempt}/${maxAttempts})。`);
  831. if (attempt < maxAttempts) {
  832. await sleep(intervalMs);
  833. }
  834. }
  835. throw lastError || new Error(`步骤 ${step}:未在 A4Sky 邮箱中找到新的匹配验证码。`);
  836. }
  837. }