inbucket-mail.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. // content/inbucket-mail.js — Content script for Inbucket polling (steps 4, 7)
  2. // Injected dynamically on the configured Inbucket host
  3. //
  4. // Supported page:
  5. // - /m/<mailbox>/
  6. const INBUCKET_PREFIX = '[MultiPage:inbucket-mail]';
  7. const isTopFrame = window === window.top;
  8. const SEEN_MAIL_IDS_KEY = 'seenInbucketMailIds';
  9. console.log(INBUCKET_PREFIX, 'Content script loaded on', location.href, 'frame:', isTopFrame ? 'top' : 'child');
  10. if (!isTopFrame) {
  11. console.log(INBUCKET_PREFIX, 'Skipping child frame');
  12. } else {
  13. let seenMailIds = new Set();
  14. async function loadSeenMailIds() {
  15. try {
  16. const data = await chrome.storage.session.get(SEEN_MAIL_IDS_KEY);
  17. if (Array.isArray(data[SEEN_MAIL_IDS_KEY])) {
  18. seenMailIds = new Set(data[SEEN_MAIL_IDS_KEY]);
  19. console.log(INBUCKET_PREFIX, `Loaded ${seenMailIds.size} previously seen mail ids`);
  20. }
  21. } catch (err) {
  22. console.warn(INBUCKET_PREFIX, 'Session storage unavailable, using in-memory seen mail ids:', err?.message || err);
  23. }
  24. }
  25. async function persistSeenMailIds() {
  26. try {
  27. await chrome.storage.session.set({ [SEEN_MAIL_IDS_KEY]: [...seenMailIds] });
  28. } catch (err) {
  29. console.warn(INBUCKET_PREFIX, 'Could not persist seen mail ids, continuing in-memory only:', err?.message || err);
  30. }
  31. }
  32. loadSeenMailIds();
  33. chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  34. if (message.type === 'POLL_EMAIL') {
  35. resetStopState();
  36. handlePollEmail(message.step, message.payload).then(result => {
  37. sendResponse(result);
  38. }).catch(err => {
  39. if (isStopError(err)) {
  40. log(`步骤 ${message.step}:已被用户停止。`, 'warn');
  41. sendResponse({ stopped: true, error: err.message });
  42. return;
  43. }
  44. log(`步骤 ${message.step}:邮箱轮询失败:${err.message}`, 'warn');
  45. sendResponse({ error: err.message });
  46. });
  47. return true;
  48. }
  49. });
  50. function normalizeText(value) {
  51. return (value || '').replace(/\s+/g, ' ').trim().toLowerCase();
  52. }
  53. function extractVerificationCode(text) {
  54. const matchCn = text.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/);
  55. if (matchCn) return matchCn[1];
  56. const matchEn = text.match(/code[:\s]+is[:\s]+(\d{6})|code[:\s]+(\d{6})/i);
  57. if (matchEn) return matchEn[1] || matchEn[2];
  58. const match6 = text.match(/\b(\d{6})\b/);
  59. if (match6) return match6[1];
  60. return null;
  61. }
  62. function rowMatchesFilters(mail, senderFilters, subjectFilters, targetEmail) {
  63. const sender = normalizeText(mail.sender);
  64. const subject = normalizeText(mail.subject);
  65. const mailbox = normalizeText(mail.mailbox);
  66. const combined = normalizeText(mail.combinedText);
  67. const targetLocal = normalizeText((targetEmail || '').split('@')[0]);
  68. const senderMatch = senderFilters.some(f => sender.includes(f.toLowerCase()) || combined.includes(f.toLowerCase()));
  69. const subjectMatch = subjectFilters.some(f => subject.includes(f.toLowerCase()) || combined.includes(f.toLowerCase()));
  70. const mailboxMatch = Boolean(targetLocal) && mailbox.includes(targetLocal);
  71. const forwardedDuck = /duckduckgo|forward(?:ed)?\s*by/i.test(mail.combinedText);
  72. const code = extractVerificationCode(mail.combinedText);
  73. const keywordMatch = /openai|chatgpt|verify|verification|confirm|login|验证码|代码/.test(combined);
  74. if (mailboxMatch) return { matched: true, mailboxMatch, code };
  75. if (senderMatch || subjectMatch) return { matched: true, mailboxMatch: false, code };
  76. if (code && (forwardedDuck || keywordMatch)) return { matched: true, mailboxMatch: false, code };
  77. return { matched: false, mailboxMatch: false, code };
  78. }
  79. function findMailboxEntries() {
  80. return document.querySelectorAll('.message-list-entry');
  81. }
  82. function getMailboxEntryId(entry, index = 0) {
  83. const explicitId = entry.getAttribute('data-id') || entry.dataset?.id || '';
  84. if (explicitId) return explicitId;
  85. const subject = entry.querySelector('.subject')?.textContent?.trim() || '';
  86. const sender = entry.querySelector('.from')?.textContent?.trim() || '';
  87. const dateText = entry.querySelector('.date')?.textContent?.trim() || '';
  88. return `mailbox:${index}:${normalizeText(subject)}|${normalizeText(sender)}|${normalizeText(dateText)}`;
  89. }
  90. function parseMailboxEntry(entry, index = 0) {
  91. const subject = entry.querySelector('.subject')?.textContent?.trim() || '';
  92. const sender = entry.querySelector('.from')?.textContent?.trim() || '';
  93. const dateText = entry.querySelector('.date')?.textContent?.trim() || '';
  94. const combinedText = [subject, sender, dateText].filter(Boolean).join(' ');
  95. return {
  96. entry,
  97. dateText,
  98. sender,
  99. mailbox: '',
  100. subject,
  101. unread: entry.classList.contains('unseen'),
  102. combinedText,
  103. mailId: getMailboxEntryId(entry, index),
  104. };
  105. }
  106. function getCurrentMailboxIds() {
  107. const ids = new Set();
  108. Array.from(findMailboxEntries()).forEach((entry, index) => {
  109. ids.add(getMailboxEntryId(entry, index));
  110. });
  111. return ids;
  112. }
  113. async function refreshMailbox() {
  114. const refreshButton = document.querySelector('button[alt="Refresh Mailbox"]');
  115. if (!refreshButton) return;
  116. simulateClick(refreshButton);
  117. await sleep(800);
  118. }
  119. async function openMailboxEntry(entry) {
  120. simulateClick(entry);
  121. for (let i = 0; i < 20; i++) {
  122. if (entry.classList.contains('selected') || document.querySelector('.message-header, .message-body, .button-bar')) {
  123. return;
  124. }
  125. await sleep(150);
  126. }
  127. }
  128. async function deleteCurrentMailboxMessage(step) {
  129. try {
  130. const deleteButton = await waitForElement('.button-bar button.danger', 5000);
  131. simulateClick(deleteButton);
  132. log(`步骤 ${step}:已删除邮箱消息`, 'ok');
  133. await sleep(1200);
  134. } catch (err) {
  135. log(`步骤 ${step}:删除邮箱消息失败:${err.message}`, 'warn');
  136. }
  137. }
  138. async function handleMailboxPollEmail(step, payload) {
  139. const {
  140. senderFilters = [],
  141. subjectFilters = [],
  142. maxAttempts = 20,
  143. intervalMs = 3000,
  144. excludeCodes = [],
  145. } = payload || {};
  146. const excludedCodeSet = new Set(excludeCodes.filter(Boolean));
  147. log(`步骤 ${step}:开始轮询 Inbucket 邮箱页面(最多 ${maxAttempts} 次)`);
  148. try {
  149. await waitForElement('.message-list, .message-list-entry', 15000);
  150. log(`步骤 ${step}:邮箱页面已加载`);
  151. } catch {
  152. throw new Error('Inbucket 邮箱页面未加载完成,请确认已打开 /m/<mailbox>/ 页面。');
  153. }
  154. const existingMailIds = getCurrentMailboxIds();
  155. log(`步骤 ${step}:已记录当前 ${existingMailIds.size} 封旧消息快照`);
  156. const FALLBACK_AFTER = 3;
  157. for (let attempt = 1; attempt <= maxAttempts; attempt++) {
  158. log(`步骤 ${step}:正在轮询 Inbucket 邮箱,第 ${attempt}/${maxAttempts} 次`);
  159. if (attempt > 1) {
  160. await refreshMailbox();
  161. }
  162. const entries = Array.from(findMailboxEntries()).map(parseMailboxEntry);
  163. const useFallback = attempt > FALLBACK_AFTER;
  164. const candidates = [];
  165. for (const mail of entries) {
  166. if (!mail.unread) continue;
  167. if (seenMailIds.has(mail.mailId)) continue;
  168. if (!useFallback && existingMailIds.has(mail.mailId)) continue;
  169. const match = rowMatchesFilters(mail, senderFilters, subjectFilters, '');
  170. if (!match.matched) continue;
  171. candidates.push({ ...mail, code: match.code });
  172. }
  173. for (const mail of candidates) {
  174. const code = mail.code || extractVerificationCode(mail.combinedText);
  175. if (!code) continue;
  176. if (excludedCodeSet.has(code)) {
  177. log(`步骤 ${step}:跳过排除的验证码:${code}`, 'info');
  178. continue;
  179. }
  180. await openMailboxEntry(mail.entry);
  181. await deleteCurrentMailboxMessage(step);
  182. seenMailIds.add(mail.mailId);
  183. await persistSeenMailIds();
  184. const source = existingMailIds.has(mail.mailId) ? '回退匹配邮件' : '新邮件';
  185. log(
  186. `步骤 ${step}:已找到验证码:${code}(来源:${source},发件人:${mail.sender || '未知'},主题:${(mail.subject || '').slice(0, 60)})`,
  187. 'ok'
  188. );
  189. return {
  190. ok: true,
  191. code,
  192. emailTimestamp: Date.now(),
  193. mailId: mail.mailId,
  194. };
  195. }
  196. if (attempt === FALLBACK_AFTER + 1) {
  197. log(`步骤 ${step}:暂未发现新消息,开始回退到较早的匹配邮件`, 'warn');
  198. }
  199. if (attempt < maxAttempts) {
  200. await sleep(intervalMs);
  201. }
  202. }
  203. throw new Error(
  204. `${(maxAttempts * intervalMs / 1000).toFixed(0)} 秒后仍未在 Inbucket 邮箱中找到匹配的验证码邮件。` +
  205. '请手动检查邮箱页面。'
  206. );
  207. }
  208. async function handlePollEmail(step, payload) {
  209. if (!location.pathname.startsWith('/m/')) {
  210. throw new Error('当前 Inbucket 仅支持 /m/<mailbox>/ 这种邮箱页面。');
  211. }
  212. return handleMailboxPollEmail(step, payload);
  213. }
  214. } // end of isTopFrame else block