qq-mail.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. // content/qq-mail.js — Content script for QQ Mail (steps 4, 7)
  2. // Injected on: mail.qq.com, wx.mail.qq.com
  3. // NOTE: all_frames: true — this script runs in every frame on QQ Mail
  4. const QQ_MAIL_PREFIX = '[MultiPage:qq-mail]';
  5. const isNewVersion = location.hostname === 'wx.mail.qq.com';
  6. const isTopFrame = window === window.top;
  7. console.log(QQ_MAIL_PREFIX, 'Content script loaded on', location.href, 'frame:', isTopFrame ? 'top' : 'child');
  8. // For old QQ Mail with iframes, only report ready from the top frame
  9. // to avoid duplicate registrations. The inbox frame will handle email ops.
  10. if (!isTopFrame && isNewVersion) {
  11. console.log(QQ_MAIL_PREFIX, 'Skipping non-top frame on new QQ Mail');
  12. // Don't do anything in child frames of new version
  13. }
  14. // ============================================================
  15. // Frame detection
  16. // ============================================================
  17. function isInboxFrame() {
  18. if (isNewVersion) return isTopFrame;
  19. // Old version: check if this frame has email list elements
  20. return !!document.querySelector(
  21. '#mailList, .mail-list, [id*="mailList"], #frm_main, .toarea, .mailList'
  22. );
  23. }
  24. // ============================================================
  25. // Login State Check
  26. // ============================================================
  27. function checkLoginState() {
  28. if (isNewVersion) {
  29. // wx.mail.qq.com: look for compose button or folder list
  30. return !!(
  31. document.querySelector('[class*="folder"], [class*="compose"], [class*="sidebar"]') ||
  32. document.querySelector('nav, [role="navigation"]') ||
  33. document.querySelector('[class*="mail-list"], [class*="mailList"]')
  34. );
  35. } else {
  36. // mail.qq.com: look for known logged-in elements
  37. return !!(
  38. document.querySelector('#folder_1, .folder_inbox, #composebtn, #mainFrameContainer')
  39. );
  40. }
  41. }
  42. // ============================================================
  43. // Message Handler
  44. // ============================================================
  45. chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  46. if (message.type === 'POLL_EMAIL') {
  47. // For old QQ Mail, only handle in the inbox frame
  48. if (!isNewVersion && !isInboxFrame()) {
  49. sendResponse({ ok: false, reason: 'wrong-frame' });
  50. return;
  51. }
  52. handlePollEmail(message.step, message.payload).then(result => {
  53. sendResponse(result);
  54. }).catch(err => {
  55. reportError(message.step, err.message);
  56. sendResponse({ error: err.message });
  57. });
  58. return true; // async response
  59. }
  60. if (message.type === 'CHECK_LOGIN') {
  61. if (!isTopFrame) {
  62. sendResponse({ loggedIn: false });
  63. return;
  64. }
  65. sendResponse({ loggedIn: checkLoginState() });
  66. return;
  67. }
  68. });
  69. // ============================================================
  70. // Email Polling
  71. // ============================================================
  72. async function handlePollEmail(step, payload) {
  73. const { filterAfterTimestamp, senderFilters, subjectFilters, maxAttempts, intervalMs } = payload;
  74. // Check login state first
  75. if (isTopFrame && !checkLoginState()) {
  76. throw new Error('QQ Mail not logged in. Please log in to QQ Mail and retry.');
  77. }
  78. log(`Step ${step}: Starting email poll (max ${maxAttempts} attempts, every ${intervalMs / 1000}s)`);
  79. for (let attempt = 1; attempt <= maxAttempts; attempt++) {
  80. log(`Polling QQ Mail... attempt ${attempt}/${maxAttempts}`);
  81. // Try to refresh inbox
  82. await refreshInbox();
  83. await sleep(800); // Wait for refresh to take effect
  84. // Search for matching email
  85. const result = await findMatchingEmail(senderFilters, subjectFilters);
  86. if (result) {
  87. log(`Step ${step}: Found matching email! Extracting code...`);
  88. const code = extractVerificationCode(result.content);
  89. if (code) {
  90. log(`Step ${step}: Verification code found: ${code}`, 'ok');
  91. return { ok: true, code, emailTimestamp: Date.now() };
  92. } else {
  93. log(`Step ${step}: Email found but no 6-digit code in content. Preview: ${result.content.slice(0, 100)}`, 'warn');
  94. }
  95. }
  96. if (attempt < maxAttempts) {
  97. await sleep(intervalMs);
  98. }
  99. }
  100. throw new Error(
  101. `No matching email found after ${(maxAttempts * intervalMs / 1000).toFixed(0)}s. ` +
  102. 'Check QQ Mail manually. Email may be in spam folder.'
  103. );
  104. }
  105. // ============================================================
  106. // Inbox Refresh
  107. // ============================================================
  108. async function refreshInbox() {
  109. if (isNewVersion) {
  110. // wx.mail.qq.com: try multiple refresh selectors
  111. const refreshBtn = document.querySelector(
  112. '[class*="refresh"], [title*="刷新"], button[aria-label*="refresh"], ' +
  113. '[data-action="refresh"], .toolbar-refresh'
  114. );
  115. if (refreshBtn) {
  116. simulateClick(refreshBtn);
  117. console.log(QQ_MAIL_PREFIX, 'Clicked refresh button (new version)');
  118. } else {
  119. // Fallback: click on "Inbox" folder to force reload
  120. const inboxLink = document.querySelector(
  121. '[class*="inbox"], [title*="收件箱"], a[href*="inbox"]'
  122. );
  123. if (inboxLink) {
  124. simulateClick(inboxLink);
  125. console.log(QQ_MAIL_PREFIX, 'Clicked inbox link to refresh');
  126. }
  127. }
  128. } else {
  129. // mail.qq.com: old version
  130. const refreshBtn = document.querySelector(
  131. '#refresh, .refresh_btn, [id*="refresh"], a[title*="刷新"]'
  132. );
  133. if (refreshBtn) {
  134. simulateClick(refreshBtn);
  135. console.log(QQ_MAIL_PREFIX, 'Clicked refresh button (old version)');
  136. }
  137. }
  138. }
  139. // ============================================================
  140. // Find Matching Email
  141. // ============================================================
  142. async function findMatchingEmail(senderFilters, subjectFilters) {
  143. let emailItems;
  144. if (isNewVersion) {
  145. // wx.mail.qq.com: email list items
  146. emailItems = document.querySelectorAll(
  147. '[class*="mail-item"], [class*="list-item"], [class*="mail_item"], ' +
  148. 'tr[class*="mail"], div[class*="letter"], [class*="thread"]'
  149. );
  150. } else {
  151. // mail.qq.com: email list inside table
  152. emailItems = document.querySelectorAll(
  153. '.toarea tr, #mailList tr, .mail_list tr, [id*="mail_"] tr'
  154. );
  155. }
  156. console.log(QQ_MAIL_PREFIX, `Found ${emailItems.length} email items to scan`);
  157. for (const item of emailItems) {
  158. const text = (item.textContent || '').toLowerCase();
  159. const senderMatch = senderFilters.some(f => text.includes(f.toLowerCase()));
  160. const subjectMatch = subjectFilters.some(f => text.includes(f.toLowerCase()));
  161. if (senderMatch || subjectMatch) {
  162. console.log(QQ_MAIL_PREFIX, 'Found matching email item:', text.slice(0, 100));
  163. // Try to get content from the visible text first
  164. let content = item.textContent || '';
  165. // Check if we can already extract a code from the preview
  166. if (extractVerificationCode(content)) {
  167. return { content };
  168. }
  169. // Need to click into the email for full body
  170. log('Clicking email to read full content...');
  171. simulateClick(item);
  172. await sleep(1500);
  173. // Read email body
  174. const bodyEl = document.querySelector(
  175. '[class*="mail-body"], [class*="mail_body"], [class*="letter-body"], ' +
  176. '.body_content, #contentDiv, [class*="read-content"], [class*="mail-detail"]'
  177. );
  178. if (bodyEl) {
  179. content = bodyEl.textContent || bodyEl.innerText || '';
  180. console.log(QQ_MAIL_PREFIX, 'Email body content:', content.slice(0, 200));
  181. }
  182. // Go back to inbox after reading
  183. await goBackToInbox();
  184. return { content };
  185. }
  186. }
  187. return null;
  188. }
  189. async function goBackToInbox() {
  190. if (isNewVersion) {
  191. // Click back or inbox link
  192. const backBtn = document.querySelector(
  193. '[class*="back"], [aria-label*="back"], [title*="返回"], [class*="return"]'
  194. );
  195. if (backBtn) {
  196. simulateClick(backBtn);
  197. await sleep(500);
  198. }
  199. }
  200. // Old version typically uses frames, no need to go back
  201. }
  202. // ============================================================
  203. // Verification Code Extraction
  204. // ============================================================
  205. function extractVerificationCode(text) {
  206. // Try various patterns for 6-digit verification codes
  207. // Pattern 1: standalone 6 digits
  208. const match6 = text.match(/\b(\d{6})\b/);
  209. if (match6) return match6[1];
  210. // Pattern 2: code/verification followed by digits
  211. const matchLabeled = text.match(/(?:code|验证码|verification|verify)[:\s]*(\d{4,8})/i);
  212. if (matchLabeled) return matchLabeled[1];
  213. // Pattern 3: digits followed by "code" label (Chinese: 是您的验证码)
  214. const matchReverse = text.match(/(\d{4,8})\s*(?:是|为|is)?\s*(?:您的)?(?:验证码|code)/i);
  215. if (matchReverse) return matchReverse[1];
  216. return null;
  217. }