icloud-mail.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. const ICLOUD_MAIL_PREFIX = '[MultiPage:icloud-mail]';
  2. const isTopFrame = window === window.top;
  3. console.log(ICLOUD_MAIL_PREFIX, 'Content script loaded on', location.href, 'frame:', isTopFrame ? 'top' : 'child');
  4. function isMailApplicationFrame() {
  5. if (/\/applications\/mail2\//.test(location.pathname)) {
  6. return true;
  7. }
  8. return Boolean(document.querySelector('.content-container, .mail-message-defaults, .thread-participants'));
  9. }
  10. if (isTopFrame) {
  11. console.log(ICLOUD_MAIL_PREFIX, 'Top frame detected; waiting for mail iframe.');
  12. } else {
  13. chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  14. if (message.type === 'POLL_EMAIL') {
  15. if (!isMailApplicationFrame()) {
  16. sendResponse({ ok: false, reason: 'wrong-frame' });
  17. return;
  18. }
  19. resetStopState();
  20. handlePollEmail(message.step, message.payload).then((result) => {
  21. sendResponse(result);
  22. }).catch((err) => {
  23. if (isStopError(err)) {
  24. log(`步骤 ${message.step}:已被用户停止。`, 'warn');
  25. sendResponse({ stopped: true, error: err.message });
  26. return;
  27. }
  28. log(`步骤 ${message.step}:iCloud 邮箱轮询失败:${err.message}`, 'warn');
  29. sendResponse({ error: err.message });
  30. });
  31. return true;
  32. }
  33. });
  34. function normalizeText(value) {
  35. return String(value || '').replace(/\s+/g, ' ').trim();
  36. }
  37. function isVisibleElement(node) {
  38. return Boolean(node instanceof HTMLElement)
  39. && (Boolean(node.offsetParent) || getComputedStyle(node).position === 'fixed');
  40. }
  41. function collectThreadItems() {
  42. return Array.from(document.querySelectorAll('.content-container')).filter((item) => {
  43. if (!isVisibleElement(item)) return false;
  44. return item.querySelector('.thread-participants')
  45. && item.querySelector('.thread-subject')
  46. && item.querySelector('.thread-preview');
  47. });
  48. }
  49. function getThreadItemMetadata(item) {
  50. const sender = normalizeText(item.querySelector('.thread-participants')?.textContent || '');
  51. const subject = normalizeText(item.querySelector('.thread-subject')?.textContent || '');
  52. const preview = normalizeText(item.querySelector('.thread-preview')?.textContent || '');
  53. const timestamp = normalizeText(item.querySelector('.thread-timestamp')?.textContent || '');
  54. return {
  55. sender,
  56. subject,
  57. preview,
  58. timestamp,
  59. combinedText: normalizeText([sender, subject, preview, timestamp].filter(Boolean).join(' ')),
  60. };
  61. }
  62. function buildItemSignature(item) {
  63. const meta = getThreadItemMetadata(item);
  64. return normalizeText([
  65. item.getAttribute('aria-label') || '',
  66. meta.sender,
  67. meta.subject,
  68. meta.preview,
  69. meta.timestamp,
  70. ].join('::')).slice(0, 240);
  71. }
  72. function extractVerificationCode(text) {
  73. const matchCn = text.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/);
  74. if (matchCn) return matchCn[1];
  75. const matchEn = text.match(/code[:\s]+is[:\s]+(\d{6})|code[:\s]+(\d{6})/i);
  76. if (matchEn) return matchEn[1] || matchEn[2];
  77. const match6 = text.match(/\b(\d{6})\b/);
  78. if (match6) return match6[1];
  79. return null;
  80. }
  81. function readOpenedMailHeader() {
  82. const headerRoot = document.querySelector('.ic-efwqa7');
  83. if (!headerRoot) {
  84. return { sender: '', recipients: '', timestamp: '' };
  85. }
  86. const contactValues = Array.from(headerRoot.querySelectorAll('.contact-token .ic-x1z554'))
  87. .map((node) => normalizeText(node.textContent))
  88. .filter(Boolean);
  89. const sender = contactValues[0] || '';
  90. const recipients = contactValues.slice(1).join(' ');
  91. const timestamp = normalizeText(headerRoot.querySelector('.ic-rffsj8')?.textContent || '');
  92. return { sender, recipients, timestamp };
  93. }
  94. function getOpenedMailBodyRoot() {
  95. return document.querySelector('.mail-message-defaults, .pane.thread-detail-pane');
  96. }
  97. function readOpenedMailBody() {
  98. const bodyRoot = getOpenedMailBodyRoot();
  99. return normalizeText(bodyRoot?.innerText || bodyRoot?.textContent || '');
  100. }
  101. function getThreadListItemRoot(item) {
  102. return item?.closest?.('.thread-list-item, [role="treeitem"]') || null;
  103. }
  104. function isThreadItemSelected(item, expectedSignature = '') {
  105. const expected = normalizeText(expectedSignature);
  106. const candidates = collectThreadItems();
  107. const matchedItem = expected
  108. ? candidates.find((candidate) => buildItemSignature(candidate) === expected)
  109. : item;
  110. const root = getThreadListItemRoot(matchedItem || item);
  111. if (!root) {
  112. return false;
  113. }
  114. if (root.getAttribute('aria-selected') === 'true') {
  115. return true;
  116. }
  117. const className = String(root.className || '').toLowerCase();
  118. return /\b(selected|current|active)\b/.test(className);
  119. }
  120. function openedMailMatchesExpectedContent(expectedMeta = {}, header = null, bodyText = '') {
  121. const expectedSender = normalizeText(expectedMeta.sender || '').toLowerCase();
  122. const expectedSubject = normalizeText(expectedMeta.subject || '').toLowerCase();
  123. const combined = normalizeText([
  124. header?.sender || '',
  125. header?.recipients || '',
  126. header?.timestamp || '',
  127. bodyText || '',
  128. ].join(' ')).toLowerCase();
  129. if (expectedSender && combined.includes(expectedSender)) {
  130. return true;
  131. }
  132. if (expectedSubject && combined.includes(expectedSubject)) {
  133. return true;
  134. }
  135. return false;
  136. }
  137. async function waitForOpenedMailContent(item, expectedMeta = {}, timeout = 10000) {
  138. const expectedSignature = buildItemSignature(item);
  139. const start = Date.now();
  140. while (Date.now() - start < timeout) {
  141. throwIfStopped();
  142. const headerRoot = document.querySelector('.ic-efwqa7');
  143. const bodyRoot = getOpenedMailBodyRoot();
  144. const selected = isThreadItemSelected(item, expectedSignature);
  145. if (selected && (headerRoot || bodyRoot)) {
  146. const header = readOpenedMailHeader();
  147. const bodyText = normalizeText(bodyRoot?.innerText || bodyRoot?.textContent || '');
  148. if (openedMailMatchesExpectedContent(expectedMeta, header, bodyText)) {
  149. return { headerRoot, bodyRoot };
  150. }
  151. }
  152. await sleep(100);
  153. }
  154. throw new Error('打开邮件后未找到详情区域,请确认邮件内容已加载。');
  155. }
  156. async function openMailItemAndRead(item) {
  157. const expectedMeta = getThreadItemMetadata(item);
  158. simulateClick(item);
  159. const { bodyRoot } = await waitForOpenedMailContent(item, expectedMeta, 10000);
  160. await sleep(300);
  161. const header = readOpenedMailHeader();
  162. const bodyText = normalizeText(
  163. bodyRoot?.innerText || bodyRoot?.textContent || readOpenedMailBody()
  164. );
  165. return {
  166. ...header,
  167. bodyText,
  168. combinedText: normalizeText([header.sender, header.recipients, header.timestamp, bodyText].filter(Boolean).join(' ')),
  169. };
  170. }
  171. async function refreshInbox() {
  172. const refreshPatterns = [/刷新/i, /refresh/i, /重新载入/i];
  173. const candidates = document.querySelectorAll('button, [role="button"], a');
  174. for (const node of candidates) {
  175. const text = normalizeText(node.innerText || node.textContent || '');
  176. const label = normalizeText(node.getAttribute('aria-label') || node.getAttribute('title') || '');
  177. if (refreshPatterns.some((pattern) => pattern.test(text) || pattern.test(label))) {
  178. simulateClick(node);
  179. await sleep(1000);
  180. return;
  181. }
  182. }
  183. const inboxPatterns = [/收件箱/, /inbox/i];
  184. for (const node of candidates) {
  185. const text = normalizeText(node.innerText || node.textContent || '');
  186. const label = normalizeText(node.getAttribute('aria-label') || node.getAttribute('title') || '');
  187. if (inboxPatterns.some((pattern) => pattern.test(text) || pattern.test(label))) {
  188. simulateClick(node);
  189. await sleep(1000);
  190. return;
  191. }
  192. }
  193. }
  194. async function handlePollEmail(step, payload) {
  195. const { senderFilters, subjectFilters, maxAttempts, intervalMs, excludeCodes = [] } = payload;
  196. const excludedCodeSet = new Set(excludeCodes.filter(Boolean));
  197. const FALLBACK_AFTER = 3;
  198. const normalizedSenderFilters = senderFilters.map((filter) => String(filter || '').toLowerCase()).filter(Boolean);
  199. const normalizedSubjectFilters = subjectFilters.map((filter) => String(filter || '').toLowerCase()).filter(Boolean);
  200. log(`步骤 ${step}:开始轮询 iCloud 邮箱(最多 ${maxAttempts} 次)`);
  201. await waitForElement('.content-container', 10000);
  202. await sleep(1500);
  203. const existingSignatures = new Set(collectThreadItems().map(buildItemSignature));
  204. log(`步骤 ${step}:已记录当前 ${existingSignatures.size} 封旧邮件快照`);
  205. for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
  206. log(`步骤 ${step}:正在轮询 iCloud 邮箱,第 ${attempt}/${maxAttempts} 次`);
  207. if (attempt > 1) {
  208. await refreshInbox();
  209. await sleep(1200);
  210. }
  211. const items = collectThreadItems();
  212. const useFallback = attempt > FALLBACK_AFTER;
  213. for (const item of items) {
  214. const signature = buildItemSignature(item);
  215. if (!useFallback && existingSignatures.has(signature)) {
  216. continue;
  217. }
  218. const meta = getThreadItemMetadata(item);
  219. const lowerSender = meta.sender.toLowerCase();
  220. const lowerSubject = normalizeText([meta.subject, meta.preview].join(' ')).toLowerCase();
  221. const senderMatch = normalizedSenderFilters.some((filter) => lowerSender.includes(filter));
  222. const subjectMatch = normalizedSubjectFilters.some((filter) => lowerSubject.includes(filter));
  223. if (!senderMatch && !subjectMatch) {
  224. continue;
  225. }
  226. let code = extractVerificationCode(meta.combinedText);
  227. let opened = null;
  228. if (!code) {
  229. opened = await openMailItemAndRead(item);
  230. const openedSender = opened.sender.toLowerCase();
  231. const openedBody = opened.bodyText.toLowerCase();
  232. const openedSenderMatch = normalizedSenderFilters.some((filter) => openedSender.includes(filter));
  233. const openedSubjectMatch = normalizedSubjectFilters.some((filter) => openedBody.includes(filter));
  234. if (!openedSenderMatch && !openedSubjectMatch && !senderMatch && !subjectMatch) {
  235. continue;
  236. }
  237. code = extractVerificationCode(opened.combinedText);
  238. }
  239. if (!code) {
  240. continue;
  241. }
  242. if (excludedCodeSet.has(code)) {
  243. log(`步骤 ${step}:跳过排除的验证码:${code}`, 'info');
  244. continue;
  245. }
  246. const source = useFallback && existingSignatures.has(signature) ? '回退匹配邮件' : '新邮件';
  247. log(`步骤 ${step}:已找到验证码:${code}(来源:${source})`, 'ok');
  248. return {
  249. ok: true,
  250. code,
  251. emailTimestamp: Date.now(),
  252. preview: (opened?.combinedText || meta.combinedText).slice(0, 160),
  253. };
  254. }
  255. if (attempt === FALLBACK_AFTER + 1) {
  256. log(`步骤 ${step}:连续 ${FALLBACK_AFTER} 次未发现新邮件,开始回退到首封匹配邮件`, 'warn');
  257. }
  258. if (attempt < maxAttempts) {
  259. await sleep(intervalMs);
  260. }
  261. }
  262. throw new Error(
  263. `${Math.round((maxAttempts * intervalMs) / 1000)} 秒后仍未在 iCloud 邮箱中找到新的匹配邮件。请手动检查收件箱。`
  264. );
  265. }
  266. }