mail-163.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. // content/mail-163.js — Content script for 163 Mail (steps 4, 7)
  2. // Injected on: mail.163.com
  3. //
  4. // DOM structure:
  5. // Mail item: div[sign="letter"] with aria-label="你的 ChatGPT 代码为 479637 发件人 : OpenAI ..."
  6. // Sender: .nui-user (e.g., "OpenAI")
  7. // Subject: span.da0 (e.g., "你的 ChatGPT 代码为 479637")
  8. // Right-click menu: .nui-menu → .nui-menu-item with text "删除邮件"
  9. const MAIL163_PREFIX = '[MultiPage:mail-163]';
  10. const isTopFrame = window === window.top;
  11. console.log(MAIL163_PREFIX, 'Content script loaded on', location.href, 'frame:', isTopFrame ? 'top' : 'child');
  12. // Only operate in the top frame
  13. if (!isTopFrame) {
  14. console.log(MAIL163_PREFIX, 'Skipping child frame');
  15. } else {
  16. // Track codes we've already seen — persisted in chrome.storage.session to survive script re-injection
  17. let seenCodes = new Set();
  18. async function loadSeenCodes() {
  19. try {
  20. const data = await chrome.storage.session.get('seenCodes');
  21. if (data.seenCodes && Array.isArray(data.seenCodes)) {
  22. seenCodes = new Set(data.seenCodes);
  23. console.log(MAIL163_PREFIX, `Loaded ${seenCodes.size} previously seen codes`);
  24. }
  25. } catch (err) {
  26. console.warn(MAIL163_PREFIX, 'Session storage unavailable, using in-memory seen codes:', err?.message || err);
  27. }
  28. }
  29. // Load previously seen codes on startup
  30. loadSeenCodes();
  31. async function persistSeenCodes() {
  32. try {
  33. await chrome.storage.session.set({ seenCodes: [...seenCodes] });
  34. } catch (err) {
  35. console.warn(MAIL163_PREFIX, 'Could not persist seen codes, continuing in-memory only:', err?.message || err);
  36. }
  37. }
  38. // ============================================================
  39. // Message Handler (top frame only)
  40. // ============================================================
  41. chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  42. if (message.type === 'POLL_EMAIL') {
  43. resetStopState();
  44. handlePollEmail(message.step, message.payload).then(result => {
  45. sendResponse(result);
  46. }).catch(err => {
  47. if (isStopError(err)) {
  48. log(`Step ${message.step}: Stopped by user.`, 'warn');
  49. sendResponse({ stopped: true, error: err.message });
  50. return;
  51. }
  52. reportError(message.step, err.message);
  53. sendResponse({ error: err.message });
  54. });
  55. return true;
  56. }
  57. });
  58. // ============================================================
  59. // Find mail items
  60. // ============================================================
  61. function parseEmailDate(item) {
  62. const aria = item.getAttribute('aria-label') || '';
  63. const m = aria.match(/(\d{4})[年/-](\d{1,2})[月/-](\d{1,2})[日]?\s*(\d{1,2}):(\d{2})/);
  64. if (m) return new Date(+m[1], m[2] - 1, +m[3], +m[4], +m[5]).getTime();
  65. const d = aria.match(/(\d{4})[年/-](\d{1,2})[月/-](\d{1,2})/);
  66. if (d) return new Date(+d[1], d[2] - 1, +d[3]).getTime();
  67. return 0;
  68. }
  69. function findMailItems() {
  70. return document.querySelectorAll('div[sign="letter"]');
  71. }
  72. function getCurrentMailIds() {
  73. const ids = new Set();
  74. findMailItems().forEach(item => {
  75. const id = item.getAttribute('id') || '';
  76. if (id) ids.add(id);
  77. });
  78. return ids;
  79. }
  80. // ============================================================
  81. // Email Polling
  82. // ============================================================
  83. async function handlePollEmail(step, payload) {
  84. const { senderFilters, subjectFilters, maxAttempts, intervalMs, filterAfterTimestamp = 0 } = payload;
  85. log(`Step ${step}: Starting email poll on 163 Mail (max ${maxAttempts} attempts)`);
  86. // Click inbox in sidebar to ensure we're in inbox view
  87. log(`Step ${step}: Waiting for sidebar...`);
  88. try {
  89. const inboxLink = await waitForElement('.nui-tree-item-text[title="收件箱"]', 5000);
  90. inboxLink.click();
  91. log(`Step ${step}: Clicked inbox`);
  92. } catch {
  93. log(`Step ${step}: Inbox link not found, proceeding...`, 'warn');
  94. }
  95. // Wait for mail list container to appear (page loaded check, inbox can be empty)
  96. log(`Step ${step}: Waiting for mail list...`);
  97. try {
  98. await waitForElement('.nui-tree-item-text[title="收件箱"], .mail-list, div[sign="letter"]', 10000);
  99. log(`Step ${step}: Mail page loaded`);
  100. } catch {
  101. log(`Step ${step}: Mail page may not be fully loaded, proceeding to poll anyway...`, 'warn');
  102. }
  103. // Snapshot existing mail IDs (may be empty if inbox is empty)
  104. const existingMailIds = getCurrentMailIds();
  105. log(`Step ${step}: Snapshotted ${existingMailIds.size} existing emails`);
  106. const FALLBACK_AFTER = 3;
  107. for (let attempt = 1; attempt <= maxAttempts; attempt++) {
  108. log(`Polling 163 Mail... attempt ${attempt}/${maxAttempts}`);
  109. await refreshInbox();
  110. await sleep(1000);
  111. const allItems = findMailItems();
  112. const useFallback = attempt > FALLBACK_AFTER;
  113. for (const item of allItems) {
  114. const id = item.getAttribute('id') || '';
  115. if (!useFallback && existingMailIds.has(id)) continue;
  116. const senderEl = item.querySelector('.nui-user');
  117. const sender = senderEl ? senderEl.textContent.toLowerCase() : '';
  118. const subjectEl = item.querySelector('span.da0');
  119. const subject = subjectEl ? subjectEl.textContent : '';
  120. const ariaLabel = (item.getAttribute('aria-label') || '').toLowerCase();
  121. const senderMatch = senderFilters.some(f => sender.includes(f.toLowerCase()) || ariaLabel.includes(f.toLowerCase()));
  122. const subjectMatch = subjectFilters.some(f => subject.toLowerCase().includes(f.toLowerCase()) || ariaLabel.includes(f.toLowerCase()));
  123. if ((senderMatch || subjectMatch) && filterAfterTimestamp > 0) {
  124. const emailTime = parseEmailDate(item);
  125. if (emailTime > 0 && emailTime < filterAfterTimestamp - 60000) {
  126. log(`Step ${step}: Skipping old email (date: ${new Date(emailTime).toLocaleString()})`, 'info');
  127. continue;
  128. }
  129. }
  130. if (senderMatch || subjectMatch) {
  131. const code = extractVerificationCode(subject + ' ' + ariaLabel);
  132. if (code && !seenCodes.has(code)) {
  133. seenCodes.add(code);
  134. persistSeenCodes();
  135. const source = useFallback && existingMailIds.has(id) ? 'fallback' : 'new';
  136. log(`Step ${step}: Code found: ${code} (${source}, subject: ${subject.slice(0, 40)})`, 'ok');
  137. // Delete this email via right-click menu, WAIT for it to finish before returning
  138. await deleteEmail(item, step);
  139. // Extra wait to ensure deletion is processed
  140. await sleep(1000);
  141. return { ok: true, code, emailTimestamp: Date.now(), mailId: id };
  142. } else if (code && seenCodes.has(code)) {
  143. log(`Step ${step}: Skipping already-seen code: ${code}`, 'info');
  144. }
  145. }
  146. }
  147. if (attempt === FALLBACK_AFTER + 1) {
  148. log(`Step ${step}: No new emails after ${FALLBACK_AFTER} attempts, falling back to first match`, 'warn');
  149. }
  150. if (attempt < maxAttempts) {
  151. await sleep(intervalMs);
  152. }
  153. }
  154. throw new Error(
  155. `No new matching email found on 163 Mail after ${(maxAttempts * intervalMs / 1000).toFixed(0)}s. ` +
  156. 'Check inbox manually.'
  157. );
  158. }
  159. // ============================================================
  160. // Delete Email via Right-Click Menu
  161. // ============================================================
  162. async function deleteEmail(item, step) {
  163. try {
  164. log(`Step ${step}: Deleting email...`);
  165. // Strategy 1: Click the trash icon inside the mail item
  166. // Each mail item has: <b class="nui-ico nui-ico-delete" title="删除邮件" sign="trash">
  167. // These icons appear on hover, so we trigger mouseover first
  168. item.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
  169. item.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
  170. await sleep(300);
  171. const trashIcon = item.querySelector('[sign="trash"], .nui-ico-delete, [title="删除邮件"]');
  172. if (trashIcon) {
  173. trashIcon.click();
  174. log(`Step ${step}: Clicked trash icon`, 'ok');
  175. await sleep(1500);
  176. // Check if item disappeared (confirm deletion)
  177. const stillExists = document.getElementById(item.id);
  178. if (!stillExists || stillExists.style.display === 'none') {
  179. log(`Step ${step}: Email deleted successfully`);
  180. } else {
  181. log(`Step ${step}: Email may not have been deleted, item still visible`, 'warn');
  182. }
  183. return;
  184. }
  185. // Strategy 2: Select checkbox then click toolbar delete button
  186. log(`Step ${step}: Trash icon not found, trying checkbox + toolbar delete...`);
  187. const checkbox = item.querySelector('[sign="checkbox"], .nui-chk');
  188. if (checkbox) {
  189. checkbox.click();
  190. await sleep(300);
  191. // Click toolbar delete button
  192. const toolbarBtns = document.querySelectorAll('.nui-btn .nui-btn-text');
  193. for (const btn of toolbarBtns) {
  194. if (btn.textContent.replace(/\s/g, '').includes('删除')) {
  195. btn.closest('.nui-btn').click();
  196. log(`Step ${step}: Clicked toolbar delete`, 'ok');
  197. await sleep(1500);
  198. return;
  199. }
  200. }
  201. }
  202. log(`Step ${step}: Could not delete email (no delete button found)`, 'warn');
  203. } catch (err) {
  204. log(`Step ${step}: Failed to delete email: ${err.message}`, 'warn');
  205. }
  206. }
  207. // ============================================================
  208. // Inbox Refresh
  209. // ============================================================
  210. async function refreshInbox() {
  211. // Try toolbar "刷 新" button
  212. const toolbarBtns = document.querySelectorAll('.nui-btn .nui-btn-text');
  213. for (const btn of toolbarBtns) {
  214. if (btn.textContent.replace(/\s/g, '') === '刷新') {
  215. btn.closest('.nui-btn').click();
  216. console.log(MAIL163_PREFIX, 'Clicked "刷新" button');
  217. await sleep(800);
  218. return;
  219. }
  220. }
  221. // Fallback: click sidebar "收 信"
  222. const shouXinBtns = document.querySelectorAll('.ra0');
  223. for (const btn of shouXinBtns) {
  224. if (btn.textContent.replace(/\s/g, '').includes('收信')) {
  225. btn.click();
  226. console.log(MAIL163_PREFIX, 'Clicked "收信" button');
  227. await sleep(800);
  228. return;
  229. }
  230. }
  231. console.log(MAIL163_PREFIX, 'Could not find refresh button');
  232. }
  233. // ============================================================
  234. // Verification Code Extraction
  235. // ============================================================
  236. function extractVerificationCode(text) {
  237. const matchCn = text.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/);
  238. if (matchCn) return matchCn[1];
  239. const matchEn = text.match(/code[:\s]+is[:\s]+(\d{6})|code[:\s]+(\d{6})/i);
  240. if (matchEn) return matchEn[1] || matchEn[2];
  241. const match6 = text.match(/\b(\d{6})\b/);
  242. if (match6) return match6[1];
  243. return null;
  244. }
  245. } // end of isTopFrame else block