phplife-mail-content.test.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  1. const test = require('node:test');
  2. const assert = require('node:assert/strict');
  3. const fs = require('node:fs');
  4. const source = fs.readFileSync('content/phplife-mail.js', 'utf8');
  5. function extractFunction(name) {
  6. const markers = [`async function ${name}(`, `function ${name}(`];
  7. const start = markers
  8. .map((marker) => source.indexOf(marker))
  9. .find((index) => index >= 0);
  10. if (start < 0) {
  11. throw new Error(`missing function ${name}`);
  12. }
  13. let parenDepth = 0;
  14. let signatureEnded = false;
  15. let braceStart = -1;
  16. for (let i = start; i < source.length; i += 1) {
  17. const ch = source[i];
  18. if (ch === '(') {
  19. parenDepth += 1;
  20. } else if (ch === ')') {
  21. parenDepth -= 1;
  22. if (parenDepth === 0) {
  23. signatureEnded = true;
  24. }
  25. } else if (ch === '{' && signatureEnded) {
  26. braceStart = i;
  27. break;
  28. }
  29. }
  30. if (braceStart < 0) {
  31. throw new Error(`missing body for function ${name}`);
  32. }
  33. let depth = 0;
  34. let end = braceStart;
  35. let inString = null;
  36. let escaped = false;
  37. for (; end < source.length; end += 1) {
  38. const ch = source[end];
  39. if (escaped) {
  40. escaped = false;
  41. continue;
  42. }
  43. if (ch === '\\') {
  44. escaped = true;
  45. continue;
  46. }
  47. if (inString) {
  48. if (ch === inString) {
  49. inString = null;
  50. }
  51. continue;
  52. }
  53. if (ch === '"' || ch === '\'' || ch === '`') {
  54. inString = ch;
  55. continue;
  56. }
  57. if (ch === '{') depth += 1;
  58. if (ch === '}') {
  59. depth -= 1;
  60. if (depth === 0) {
  61. end += 1;
  62. break;
  63. }
  64. }
  65. }
  66. return source.slice(start, end);
  67. }
  68. test('phplife mail parser extracts code and recipient from roundcube message view', () => {
  69. const bundle = [
  70. extractFunction('normalizeText'),
  71. extractFunction('parseRoundcubeTimestamp'),
  72. extractFunction('extractVerificationCodes'),
  73. extractFunction('getMessageDetailsFromDocument'),
  74. ].join('\n');
  75. const api = new Function(`${bundle}
  76. return { getMessageDetailsFromDocument, parseRoundcubeTimestamp, extractVerificationCodes };
  77. `)();
  78. const selectors = {
  79. 'h2.subject': { textContent: 'Your ChatGPT code is 866785' },
  80. '.headers-table .header.from': { textContent: 'noreply@tm.openai.com' },
  81. '.headers-table .header.to': { textContent: 'n2026041807@a4sky.com' },
  82. '.headers-table .header.date': { textContent: '今天 12:30' },
  83. '#messagebody': {
  84. innerText: 'Enter this temporary verification code to continue: 866785. ChatGPT Log-in Code 866785',
  85. textContent: 'Enter this temporary verification code to continue: 866785. ChatGPT Log-in Code 866785',
  86. },
  87. };
  88. const fakeDocument = {
  89. querySelector(selector) {
  90. return selectors[selector] || null;
  91. },
  92. body: {
  93. innerText: '',
  94. textContent: '',
  95. },
  96. };
  97. const details = api.getMessageDetailsFromDocument(fakeDocument);
  98. assert.equal(details.subject, 'Your ChatGPT code is 866785');
  99. assert.equal(details.from, 'noreply@tm.openai.com');
  100. assert.equal(details.to, 'n2026041807@a4sky.com');
  101. assert.equal(details.codes[0], '866785');
  102. assert.equal(Number.isFinite(details.emailTimestamp), true);
  103. });
  104. test('phplife mail parser can read verification code directly from list row title', () => {
  105. const bundle = [
  106. extractFunction('normalizeText'),
  107. extractFunction('parseRoundcubeTimestamp'),
  108. extractFunction('extractVerificationCodes'),
  109. extractFunction('getRowText'),
  110. extractFunction('getRowDetails'),
  111. ].join('\n');
  112. const api = new Function(`${bundle}
  113. return { getRowDetails };
  114. `)();
  115. const subjectNode = {
  116. getAttribute(name) {
  117. return name === 'title' ? 'Your ChatGPT code is 866785' : '';
  118. },
  119. textContent: 'fallback subject',
  120. };
  121. const fromNode = { getAttribute() { return ''; }, textContent: 'noreply@tm.openai.com' };
  122. const toNode = { getAttribute() { return ''; }, textContent: 'n2026041807@a4sky.com' };
  123. const dateNode = { getAttribute() { return ''; }, textContent: '今天 12:30' };
  124. const fakeRow = {
  125. querySelector(selector) {
  126. if (selector === 'td.subject') return subjectNode;
  127. if (selector === 'td.fromto') return fromNode;
  128. if (selector === 'td.to') return toNode;
  129. if (selector === 'td.date') return dateNode;
  130. return null;
  131. },
  132. textContent: 'Your ChatGPT code is 866785 noreply@tm.openai.com n2026041807@a4sky.com 今天 12:30',
  133. };
  134. const details = api.getRowDetails(fakeRow);
  135. assert.equal(details.subject, 'Your ChatGPT code is 866785');
  136. assert.equal(details.codes[0], '866785');
  137. });
  138. test('phplife refreshMessageList clicks inbox before refresh button', async () => {
  139. const bundle = [
  140. extractFunction('ensureInboxActive'),
  141. extractFunction('refreshMessageList'),
  142. ].join('\n');
  143. const api = new Function(`${bundle}
  144. const clickOrder = [];
  145. const inboxLink = {
  146. closest() {
  147. return { classList: { contains() { return true; } } };
  148. },
  149. click() {
  150. clickOrder.push('inbox');
  151. },
  152. };
  153. const refreshButton = {
  154. click() {
  155. clickOrder.push('refresh');
  156. },
  157. };
  158. function findInboxLink() {
  159. return inboxLink;
  160. }
  161. function findRefreshButton() {
  162. return refreshButton;
  163. }
  164. async function sleep() {}
  165. return {
  166. refreshMessageList,
  167. getClickOrder() {
  168. return clickOrder.slice();
  169. },
  170. };
  171. `)();
  172. await api.refreshMessageList();
  173. assert.deepEqual(api.getClickOrder(), ['inbox', 'refresh']);
  174. });
  175. test('phplife temporary login title opens detail to extract verification code', async () => {
  176. const bundle = [
  177. extractFunction('normalizeText'),
  178. extractFunction('normalizeMinuteTimestamp'),
  179. extractFunction('shouldOpenRowForCodeDetection'),
  180. extractFunction('selectCandidateCode'),
  181. extractFunction('matchesCurrentMessage'),
  182. extractFunction('scoreRowCandidate'),
  183. extractFunction('tryOpenRowsAndRead'),
  184. ].join('\n');
  185. const api = new Function(`${bundle}
  186. let opened = 0;
  187. const seenCodes = new Set();
  188. function throwIfStopped() {}
  189. function getMessageListRows() {
  190. return [{ id: 'row-1' }];
  191. }
  192. function getRowDetails() {
  193. return {
  194. row: { id: 'row-1' },
  195. subject: 'Your temporary ChatGPT login code',
  196. from: 'noreply@tm.openai.com',
  197. to: 'n2026041807@a4sky.com',
  198. dateText: '今天 12:30',
  199. emailTimestamp: Date.now(),
  200. codes: [],
  201. combinedText: 'Your temporary ChatGPT login code noreply@tm.openai.com n2026041807@a4sky.com',
  202. };
  203. }
  204. function matchesMailFilters() {
  205. return true;
  206. }
  207. function getTargetEmailMatchState() {
  208. return { matches: true, hasExplicitEmail: true };
  209. }
  210. function log() {}
  211. function getCurrentMessageUid() {
  212. return '';
  213. }
  214. function openMessageRow() {
  215. opened += 1;
  216. }
  217. async function sleep() {}
  218. async function waitForPreviewLoaded() {
  219. return {
  220. subject: 'Your temporary ChatGPT login code',
  221. combinedText: 'Your temporary ChatGPT login code 998877 noreply@tm.openai.com n2026041807@a4sky.com',
  222. emailTimestamp: Date.now(),
  223. codes: ['998877'],
  224. };
  225. }
  226. return {
  227. tryOpenRowsAndRead,
  228. getOpenedCount() {
  229. return opened;
  230. },
  231. };
  232. `)();
  233. const result = await api.tryOpenRowsAndRead(4, {
  234. senderFilters: ['openai'],
  235. subjectFilters: ['login', 'code'],
  236. targetEmail: 'n2026041807@a4sky.com',
  237. }, new Set(), 0);
  238. assert.equal(result.code, '998877');
  239. assert.equal(api.getOpenedCount(), 1);
  240. });
  241. test('phplife Chinese temporary login title also opens detail to extract verification code', async () => {
  242. const bundle = [
  243. extractFunction('normalizeText'),
  244. extractFunction('normalizeMinuteTimestamp'),
  245. extractFunction('shouldOpenRowForCodeDetection'),
  246. extractFunction('selectCandidateCode'),
  247. extractFunction('matchesCurrentMessage'),
  248. extractFunction('scoreRowCandidate'),
  249. extractFunction('tryOpenRowsAndRead'),
  250. ].join('\n');
  251. const api = new Function(`${bundle}
  252. let opened = 0;
  253. const seenCodes = new Set();
  254. function throwIfStopped() {}
  255. function getMessageListRows() {
  256. return [{ id: 'row-1' }];
  257. }
  258. function getRowDetails() {
  259. return {
  260. row: { id: 'row-1' },
  261. subject: '你的临时 ChatGPT 登录代码',
  262. from: 'noreply@tm.openai.com',
  263. to: 'n2026041807@a4sky.com',
  264. dateText: '今天 12:30',
  265. emailTimestamp: Date.now(),
  266. codes: [],
  267. combinedText: '你的临时 ChatGPT 登录代码 noreply@tm.openai.com n2026041807@a4sky.com',
  268. };
  269. }
  270. function matchesMailFilters() {
  271. return true;
  272. }
  273. function getTargetEmailMatchState() {
  274. return { matches: true, hasExplicitEmail: true };
  275. }
  276. function log() {}
  277. function getCurrentMessageUid() {
  278. return '';
  279. }
  280. function openMessageRow() {
  281. opened += 1;
  282. }
  283. async function sleep() {}
  284. async function waitForPreviewLoaded() {
  285. return {
  286. subject: '你的临时 ChatGPT 登录代码',
  287. combinedText: '你的临时 ChatGPT 登录代码 112233 noreply@tm.openai.com n2026041807@a4sky.com',
  288. emailTimestamp: Date.now(),
  289. codes: ['112233'],
  290. };
  291. }
  292. return {
  293. tryOpenRowsAndRead,
  294. getOpenedCount() {
  295. return opened;
  296. },
  297. };
  298. `)();
  299. const result = await api.tryOpenRowsAndRead(4, {
  300. senderFilters: ['openai'],
  301. subjectFilters: ['login', 'code', '登录', '代码'],
  302. targetEmail: 'n2026041807@a4sky.com',
  303. }, new Set(), 0);
  304. assert.equal(result.code, '112233');
  305. assert.equal(api.getOpenedCount(), 1);
  306. });
  307. test('phplife remote payload parser extracts target mailbox verification code directly from refresh API payload', () => {
  308. const bundle = [
  309. extractFunction('normalizeText'),
  310. extractFunction('parseRoundcubeTimestamp'),
  311. extractFunction('extractVerificationCodes'),
  312. extractFunction('extractEmails'),
  313. extractFunction('collectPayloadStringFragments'),
  314. extractFunction('extractRemoteExecText'),
  315. extractFunction('readBalancedJsonObject'),
  316. extractFunction('parseRemoteMessageRowEntries'),
  317. extractFunction('extractRemotePayloadFragments'),
  318. extractFunction('getTargetEmailMatchState'),
  319. extractFunction('matchesMailFilters'),
  320. extractFunction('normalizeFragmentText'),
  321. extractFunction('buildRemoteRowDetails'),
  322. extractFunction('findExactTargetRemoteRows'),
  323. extractFunction('scoreRowCandidate'),
  324. extractFunction('normalizeMinuteTimestamp'),
  325. extractFunction('matchesCurrentMessage'),
  326. extractFunction('shouldOpenRowForCodeDetection'),
  327. extractFunction('scoreRemotePayloadCandidate'),
  328. extractFunction('selectCandidateCode'),
  329. extractFunction('findVerificationCodeFromRemotePayload'),
  330. ].join('\n');
  331. const api = new Function(`${bundle}
  332. const seenCodes = new Set();
  333. class DOMParser {
  334. parseFromString(text) {
  335. return {
  336. body: { textContent: String(text || '').replace(/<[^>]+>/g, ' ') },
  337. documentElement: { textContent: String(text || '').replace(/<[^>]+>/g, ' ') },
  338. };
  339. }
  340. }
  341. return { findVerificationCodeFromRemotePayload };
  342. `)();
  343. const payload = JSON.stringify({
  344. exec: [
  345. "<tr><td class='subject' title='Your ChatGPT code is 445566'>Your ChatGPT code is 445566</td><td class='to'>n20260419170738@a4sky.com</td></tr>",
  346. ],
  347. });
  348. const result = api.findVerificationCodeFromRemotePayload(payload, {
  349. senderFilters: ['openai'],
  350. subjectFilters: ['code', 'verification', '验证码'],
  351. targetEmail: 'n20260419170738@a4sky.com',
  352. }, new Set(), 0);
  353. assert.equal(result.code, '445566');
  354. });
  355. test('phplife remote payload parser ignores codes for other mailbox addresses', () => {
  356. const bundle = [
  357. extractFunction('normalizeText'),
  358. extractFunction('parseRoundcubeTimestamp'),
  359. extractFunction('extractVerificationCodes'),
  360. extractFunction('extractEmails'),
  361. extractFunction('collectPayloadStringFragments'),
  362. extractFunction('extractRemoteExecText'),
  363. extractFunction('readBalancedJsonObject'),
  364. extractFunction('parseRemoteMessageRowEntries'),
  365. extractFunction('extractRemotePayloadFragments'),
  366. extractFunction('getTargetEmailMatchState'),
  367. extractFunction('matchesMailFilters'),
  368. extractFunction('normalizeFragmentText'),
  369. extractFunction('buildRemoteRowDetails'),
  370. extractFunction('findExactTargetRemoteRows'),
  371. extractFunction('scoreRowCandidate'),
  372. extractFunction('normalizeMinuteTimestamp'),
  373. extractFunction('matchesCurrentMessage'),
  374. extractFunction('shouldOpenRowForCodeDetection'),
  375. extractFunction('scoreRemotePayloadCandidate'),
  376. extractFunction('selectCandidateCode'),
  377. extractFunction('findVerificationCodeFromRemotePayload'),
  378. ].join('\n');
  379. const api = new Function(`${bundle}
  380. const seenCodes = new Set();
  381. class DOMParser {
  382. parseFromString(text) {
  383. return {
  384. body: { textContent: String(text || '').replace(/<[^>]+>/g, ' ') },
  385. documentElement: { textContent: String(text || '').replace(/<[^>]+>/g, ' ') },
  386. };
  387. }
  388. }
  389. return { findVerificationCodeFromRemotePayload };
  390. `)();
  391. const payload = JSON.stringify({
  392. exec: [
  393. "<tr><td class='subject'>Your ChatGPT code is 111111</td><td class='to'>other@a4sky.com</td></tr>",
  394. "<tr><td class='subject'>Your ChatGPT code is 222222</td><td class='to'>target@a4sky.com</td></tr>",
  395. ],
  396. });
  397. const result = api.findVerificationCodeFromRemotePayload(payload, {
  398. senderFilters: ['openai'],
  399. subjectFilters: ['code', 'verification', '验证码'],
  400. targetEmail: 'target@a4sky.com',
  401. }, new Set(), 0);
  402. assert.equal(result.code, '222222');
  403. });
  404. test('phplife remote payload parser does not fall back to other mailbox rows when target mailbox rows exist without direct code', () => {
  405. const bundle = [
  406. extractFunction('normalizeText'),
  407. extractFunction('parseRoundcubeTimestamp'),
  408. extractFunction('extractVerificationCodes'),
  409. extractFunction('extractEmails'),
  410. extractFunction('collectPayloadStringFragments'),
  411. extractFunction('extractRemoteExecText'),
  412. extractFunction('readBalancedJsonObject'),
  413. extractFunction('parseRemoteMessageRowEntries'),
  414. extractFunction('extractRemotePayloadFragments'),
  415. extractFunction('getTargetEmailMatchState'),
  416. extractFunction('matchesMailFilters'),
  417. extractFunction('normalizeFragmentText'),
  418. extractFunction('buildRemoteRowDetails'),
  419. extractFunction('findExactTargetRemoteRows'),
  420. extractFunction('scoreRowCandidate'),
  421. extractFunction('normalizeMinuteTimestamp'),
  422. extractFunction('matchesCurrentMessage'),
  423. extractFunction('shouldOpenRowForCodeDetection'),
  424. extractFunction('scoreRemotePayloadCandidate'),
  425. extractFunction('selectCandidateCode'),
  426. extractFunction('findVerificationCodeFromRemotePayload'),
  427. ].join('\n');
  428. const api = new Function(`${bundle}
  429. const seenCodes = new Set();
  430. class DOMParser {
  431. parseFromString(text) {
  432. return {
  433. body: { textContent: String(text || '').replace(/<[^>]+>/g, ' ') },
  434. documentElement: { textContent: String(text || '').replace(/<[^>]+>/g, ' ') },
  435. };
  436. }
  437. }
  438. return { findVerificationCodeFromRemotePayload };
  439. `)();
  440. const payload = JSON.stringify({
  441. exec: [
  442. "this.add_message_row(900,{\"subject\":\"Your temporary ChatGPT login code\",\"fromto\":\"<span title=\\\"otp@tm1.openai.com\\\">OpenAI<\\/span>\",\"date\":\"今天 18:24\",\"to\":\"<span title=\\\"target@a4sky.com\\\">target@a4sky.com<\\/span>\"},{\"ctype\":\"text/html\",\"mbox\":\"INBOX\"},false);",
  443. "this.add_message_row(899,{\"subject\":\"Your ChatGPT code is 999999\",\"fromto\":\"<span title=\\\"otp@tm1.openai.com\\\">OpenAI<\\/span>\",\"date\":\"今天 18:24\",\"to\":\"<span title=\\\"other@a4sky.com\\\">other@a4sky.com<\\/span>\"},{\"ctype\":\"text/html\",\"mbox\":\"INBOX\"},false);",
  444. ],
  445. });
  446. const result = api.findVerificationCodeFromRemotePayload(payload, {
  447. senderFilters: ['openai'],
  448. subjectFilters: ['code', 'verification', '验证码'],
  449. targetEmail: 'target@a4sky.com',
  450. }, new Set(), 0);
  451. assert.equal(result, null);
  452. });
  453. test('phplife tryReadRemoteMessageList prefers search payload scoped to target mailbox', async () => {
  454. const bundle = [
  455. extractFunction('normalizeText'),
  456. extractFunction('parseRoundcubeTimestamp'),
  457. extractFunction('extractVerificationCodes'),
  458. extractFunction('extractEmails'),
  459. extractFunction('collectPayloadStringFragments'),
  460. extractFunction('extractRemoteExecText'),
  461. extractFunction('readBalancedJsonObject'),
  462. extractFunction('parseRemoteMessageRowEntries'),
  463. extractFunction('extractRemotePayloadFragments'),
  464. extractFunction('getTargetEmailMatchState'),
  465. extractFunction('matchesMailFilters'),
  466. extractFunction('normalizeFragmentText'),
  467. extractFunction('buildRemoteRowDetails'),
  468. extractFunction('findExactTargetRemoteRows'),
  469. extractFunction('scoreRowCandidate'),
  470. extractFunction('normalizeMinuteTimestamp'),
  471. extractFunction('matchesCurrentMessage'),
  472. extractFunction('shouldOpenRowForCodeDetection'),
  473. extractFunction('scoreRemotePayloadCandidate'),
  474. extractFunction('selectCandidateCode'),
  475. extractFunction('findVerificationCodeFromRemotePayload'),
  476. extractFunction('tryReadRemoteMessageList'),
  477. ].join('\n');
  478. const api = new Function(`${bundle}
  479. const PHPLIFE_MAIL_PREFIX = '[MultiPage:mail-phplife]';
  480. function log() {}
  481. const seenCodes = new Set();
  482. class DOMParser {
  483. parseFromString(text) {
  484. return {
  485. body: { textContent: String(text || '').replace(/<[^>]+>/g, ' ') },
  486. documentElement: { textContent: String(text || '').replace(/<[^>]+>/g, ' ') },
  487. };
  488. }
  489. }
  490. async function requestRemoteMessageSearch() {
  491. const row = {
  492. subject: '你的 ChatGPT 代码为 991895',
  493. fromto: '<span title="otp@tm1.openai.com">OpenAI</span>',
  494. date: '今天 18:56',
  495. to: '<span title="n20260419185525@a4sky.com">n20260419185525@a4sky.com</span>',
  496. };
  497. const meta = {
  498. ctype: 'text/html',
  499. mbox: 'INBOX',
  500. };
  501. return JSON.stringify({
  502. exec: [
  503. 'this.add_message_row(524,' + JSON.stringify(row) + ',' + JSON.stringify(meta) + ',false);'
  504. ],
  505. });
  506. }
  507. async function requestRemoteMessageList() {
  508. throw new Error('list api should not be needed when search already matched');
  509. }
  510. return { tryReadRemoteMessageList };
  511. `)();
  512. const result = await api.tryReadRemoteMessageList(4, {
  513. senderFilters: ['openai'],
  514. subjectFilters: ['code', 'verification', '验证码'],
  515. targetEmail: 'n20260419185525@a4sky.com',
  516. filterAfterTimestamp: 0,
  517. }, new Set());
  518. assert.equal(result.code, '991895');
  519. });