phplife-mail-content.test.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  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. for (; end < source.length; end += 1) {
  36. const ch = source[end];
  37. if (ch === '{') depth += 1;
  38. if (ch === '}') {
  39. depth -= 1;
  40. if (depth === 0) {
  41. end += 1;
  42. break;
  43. }
  44. }
  45. }
  46. return source.slice(start, end);
  47. }
  48. test('phplife mail parser extracts code and recipient from roundcube message view', () => {
  49. const bundle = [
  50. extractFunction('normalizeText'),
  51. extractFunction('parseRoundcubeTimestamp'),
  52. extractFunction('extractVerificationCodes'),
  53. extractFunction('getMessageDetailsFromDocument'),
  54. ].join('\n');
  55. const api = new Function(`${bundle}
  56. return { getMessageDetailsFromDocument, parseRoundcubeTimestamp, extractVerificationCodes };
  57. `)();
  58. const selectors = {
  59. 'h2.subject': { textContent: 'Your ChatGPT code is 866785' },
  60. '.headers-table .header.from': { textContent: 'noreply@tm.openai.com' },
  61. '.headers-table .header.to': { textContent: 'n2026041807@a4sky.com' },
  62. '.headers-table .header.date': { textContent: '今天 12:30' },
  63. '#messagebody': {
  64. innerText: 'Enter this temporary verification code to continue: 866785. ChatGPT Log-in Code 866785',
  65. textContent: 'Enter this temporary verification code to continue: 866785. ChatGPT Log-in Code 866785',
  66. },
  67. };
  68. const fakeDocument = {
  69. querySelector(selector) {
  70. return selectors[selector] || null;
  71. },
  72. body: {
  73. innerText: '',
  74. textContent: '',
  75. },
  76. };
  77. const details = api.getMessageDetailsFromDocument(fakeDocument);
  78. assert.equal(details.subject, 'Your ChatGPT code is 866785');
  79. assert.equal(details.from, 'noreply@tm.openai.com');
  80. assert.equal(details.to, 'n2026041807@a4sky.com');
  81. assert.equal(details.codes[0], '866785');
  82. assert.equal(Number.isFinite(details.emailTimestamp), true);
  83. });
  84. test('phplife mail parser can read verification code directly from list row title', () => {
  85. const bundle = [
  86. extractFunction('normalizeText'),
  87. extractFunction('parseRoundcubeTimestamp'),
  88. extractFunction('extractVerificationCodes'),
  89. extractFunction('getRowText'),
  90. extractFunction('getRowDetails'),
  91. ].join('\n');
  92. const api = new Function(`${bundle}
  93. return { getRowDetails };
  94. `)();
  95. const subjectNode = {
  96. getAttribute(name) {
  97. return name === 'title' ? 'Your ChatGPT code is 866785' : '';
  98. },
  99. textContent: 'fallback subject',
  100. };
  101. const fromNode = { getAttribute() { return ''; }, textContent: 'noreply@tm.openai.com' };
  102. const toNode = { getAttribute() { return ''; }, textContent: 'n2026041807@a4sky.com' };
  103. const dateNode = { getAttribute() { return ''; }, textContent: '今天 12:30' };
  104. const fakeRow = {
  105. querySelector(selector) {
  106. if (selector === 'td.subject') return subjectNode;
  107. if (selector === 'td.fromto') return fromNode;
  108. if (selector === 'td.to') return toNode;
  109. if (selector === 'td.date') return dateNode;
  110. return null;
  111. },
  112. textContent: 'Your ChatGPT code is 866785 noreply@tm.openai.com n2026041807@a4sky.com 今天 12:30',
  113. };
  114. const details = api.getRowDetails(fakeRow);
  115. assert.equal(details.subject, 'Your ChatGPT code is 866785');
  116. assert.equal(details.codes[0], '866785');
  117. });
  118. test('phplife refreshMessageList clicks inbox before refresh button', async () => {
  119. const bundle = [
  120. extractFunction('ensureInboxActive'),
  121. extractFunction('refreshMessageList'),
  122. ].join('\n');
  123. const api = new Function(`${bundle}
  124. const clickOrder = [];
  125. const inboxLink = {
  126. closest() {
  127. return { classList: { contains() { return true; } } };
  128. },
  129. click() {
  130. clickOrder.push('inbox');
  131. },
  132. };
  133. const refreshButton = {
  134. click() {
  135. clickOrder.push('refresh');
  136. },
  137. };
  138. function findInboxLink() {
  139. return inboxLink;
  140. }
  141. function findRefreshButton() {
  142. return refreshButton;
  143. }
  144. async function sleep() {}
  145. return {
  146. refreshMessageList,
  147. getClickOrder() {
  148. return clickOrder.slice();
  149. },
  150. };
  151. `)();
  152. await api.refreshMessageList();
  153. assert.deepEqual(api.getClickOrder(), ['inbox', 'refresh']);
  154. });
  155. test('phplife temporary login title opens detail to extract verification code', async () => {
  156. const bundle = [
  157. extractFunction('normalizeText'),
  158. extractFunction('normalizeMinuteTimestamp'),
  159. extractFunction('shouldOpenRowForCodeDetection'),
  160. extractFunction('selectCandidateCode'),
  161. extractFunction('matchesCurrentMessage'),
  162. extractFunction('scoreRowCandidate'),
  163. extractFunction('tryOpenRowsAndRead'),
  164. ].join('\n');
  165. const api = new Function(`${bundle}
  166. let opened = 0;
  167. const seenCodes = new Set();
  168. function throwIfStopped() {}
  169. function getMessageListRows() {
  170. return [{ id: 'row-1' }];
  171. }
  172. function getRowDetails() {
  173. return {
  174. row: { id: 'row-1' },
  175. subject: 'Your temporary ChatGPT login code',
  176. from: 'noreply@tm.openai.com',
  177. to: 'n2026041807@a4sky.com',
  178. dateText: '今天 12:30',
  179. emailTimestamp: Date.now(),
  180. codes: [],
  181. combinedText: 'Your temporary ChatGPT login code noreply@tm.openai.com n2026041807@a4sky.com',
  182. };
  183. }
  184. function matchesMailFilters() {
  185. return true;
  186. }
  187. function getTargetEmailMatchState() {
  188. return { matches: true, hasExplicitEmail: true };
  189. }
  190. function log() {}
  191. function getCurrentMessageUid() {
  192. return '';
  193. }
  194. function openMessageRow() {
  195. opened += 1;
  196. }
  197. async function sleep() {}
  198. async function waitForPreviewLoaded() {
  199. return {
  200. subject: 'Your temporary ChatGPT login code',
  201. combinedText: 'Your temporary ChatGPT login code 998877 noreply@tm.openai.com n2026041807@a4sky.com',
  202. emailTimestamp: Date.now(),
  203. codes: ['998877'],
  204. };
  205. }
  206. return {
  207. tryOpenRowsAndRead,
  208. getOpenedCount() {
  209. return opened;
  210. },
  211. };
  212. `)();
  213. const result = await api.tryOpenRowsAndRead(4, {
  214. senderFilters: ['openai'],
  215. subjectFilters: ['login', 'code'],
  216. targetEmail: 'n2026041807@a4sky.com',
  217. }, new Set(), 0);
  218. assert.equal(result.code, '998877');
  219. assert.equal(api.getOpenedCount(), 1);
  220. });
  221. test('phplife Chinese temporary login title also opens detail to extract verification code', async () => {
  222. const bundle = [
  223. extractFunction('normalizeText'),
  224. extractFunction('normalizeMinuteTimestamp'),
  225. extractFunction('shouldOpenRowForCodeDetection'),
  226. extractFunction('selectCandidateCode'),
  227. extractFunction('matchesCurrentMessage'),
  228. extractFunction('scoreRowCandidate'),
  229. extractFunction('tryOpenRowsAndRead'),
  230. ].join('\n');
  231. const api = new Function(`${bundle}
  232. let opened = 0;
  233. const seenCodes = new Set();
  234. function throwIfStopped() {}
  235. function getMessageListRows() {
  236. return [{ id: 'row-1' }];
  237. }
  238. function getRowDetails() {
  239. return {
  240. row: { id: 'row-1' },
  241. subject: '你的临时 ChatGPT 登录代码',
  242. from: 'noreply@tm.openai.com',
  243. to: 'n2026041807@a4sky.com',
  244. dateText: '今天 12:30',
  245. emailTimestamp: Date.now(),
  246. codes: [],
  247. combinedText: '你的临时 ChatGPT 登录代码 noreply@tm.openai.com n2026041807@a4sky.com',
  248. };
  249. }
  250. function matchesMailFilters() {
  251. return true;
  252. }
  253. function getTargetEmailMatchState() {
  254. return { matches: true, hasExplicitEmail: true };
  255. }
  256. function log() {}
  257. function getCurrentMessageUid() {
  258. return '';
  259. }
  260. function openMessageRow() {
  261. opened += 1;
  262. }
  263. async function sleep() {}
  264. async function waitForPreviewLoaded() {
  265. return {
  266. subject: '你的临时 ChatGPT 登录代码',
  267. combinedText: '你的临时 ChatGPT 登录代码 112233 noreply@tm.openai.com n2026041807@a4sky.com',
  268. emailTimestamp: Date.now(),
  269. codes: ['112233'],
  270. };
  271. }
  272. return {
  273. tryOpenRowsAndRead,
  274. getOpenedCount() {
  275. return opened;
  276. },
  277. };
  278. `)();
  279. const result = await api.tryOpenRowsAndRead(4, {
  280. senderFilters: ['openai'],
  281. subjectFilters: ['login', 'code', '登录', '代码'],
  282. targetEmail: 'n2026041807@a4sky.com',
  283. }, new Set(), 0);
  284. assert.equal(result.code, '112233');
  285. assert.equal(api.getOpenedCount(), 1);
  286. });