hotmail-utils.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. (function hotmailUtilsModule(root, factory) {
  2. if (typeof module !== 'undefined' && module.exports) {
  3. module.exports = factory();
  4. return;
  5. }
  6. root.HotmailUtils = factory();
  7. })(typeof self !== 'undefined' ? self : globalThis, function createHotmailUtils() {
  8. const HOTMAIL_MAIL_API_URL = 'https://apple.882263.xyz/api/mail-new';
  9. function normalizeText(value) {
  10. return String(value || '')
  11. .replace(/\s+/g, ' ')
  12. .trim()
  13. .toLowerCase();
  14. }
  15. function normalizeTimestamp(value) {
  16. if (!value) return 0;
  17. if (typeof value === 'number' && Number.isFinite(value)) {
  18. return value > 0 ? value : 0;
  19. }
  20. const timestamp = Date.parse(value);
  21. return Number.isFinite(timestamp) ? timestamp : 0;
  22. }
  23. function extractVerificationCode(text) {
  24. const source = String(text || '');
  25. const matchCn = source.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/i);
  26. if (matchCn) return matchCn[1];
  27. const matchEn = source.match(/code(?:\s+is|[\s:])+(\d{6})/i);
  28. if (matchEn) return matchEn[1];
  29. const matchStandalone = source.match(/\b(\d{6})\b/);
  30. return matchStandalone ? matchStandalone[1] : null;
  31. }
  32. function extractVerificationCodeFromMessage(message = {}) {
  33. const sender = firstNonEmptyString([
  34. message?.from?.emailAddress?.address,
  35. message?.sender,
  36. message?.from,
  37. ]);
  38. const subject = firstNonEmptyString([message?.subject]);
  39. const preview = firstNonEmptyString([message?.bodyPreview, message?.preview, message?.text]);
  40. return extractVerificationCode([subject, preview, sender].filter(Boolean).join(' '));
  41. }
  42. function getLatestHotmailMessage(messages) {
  43. return (Array.isArray(messages) ? messages : [])
  44. .slice()
  45. .sort((left, right) => {
  46. const leftTime = normalizeTimestamp(left?.receivedDateTime);
  47. const rightTime = normalizeTimestamp(right?.receivedDateTime);
  48. return rightTime - leftTime;
  49. })[0] || null;
  50. }
  51. function getHotmailListToggleLabel(expanded, count = 0) {
  52. const normalizedCount = Number.isFinite(Number(count)) ? Math.max(0, Number(count)) : 0;
  53. const suffix = normalizedCount > 0 ? `(${normalizedCount})` : '';
  54. return `${expanded ? '收起列表' : '展开列表'}${suffix}`;
  55. }
  56. function filterHotmailAccountsByUsage(accounts, mode = 'all') {
  57. const list = Array.isArray(accounts) ? accounts.slice() : [];
  58. if (mode === 'used') {
  59. return list.filter((account) => Boolean(account?.used));
  60. }
  61. return list;
  62. }
  63. function getHotmailBulkActionLabel(mode = 'all', count = 0) {
  64. const normalizedCount = Number.isFinite(Number(count)) ? Math.max(0, Number(count)) : 0;
  65. const prefix = mode === 'used' ? '清空已用' : '全部删除';
  66. const suffix = normalizedCount > 0 ? `(${normalizedCount})` : '';
  67. return `${prefix}${suffix}`;
  68. }
  69. function isAuthorizedHotmailAccount(account) {
  70. return Boolean(account)
  71. && account.status === 'authorized'
  72. && !account.used
  73. && Boolean(account.refreshToken);
  74. }
  75. function shouldClearHotmailCurrentSelection(account) {
  76. return Boolean(account) && account.used === true;
  77. }
  78. function upsertHotmailAccountInList(accounts, nextAccount) {
  79. const list = Array.isArray(accounts) ? accounts.slice() : [];
  80. if (!nextAccount?.id) return list;
  81. const existingIndex = list.findIndex((account) => account?.id === nextAccount.id);
  82. if (existingIndex === -1) {
  83. list.push(nextAccount);
  84. return list;
  85. }
  86. list[existingIndex] = nextAccount;
  87. return list;
  88. }
  89. function pickHotmailAccountForRun(accounts, options = {}) {
  90. const candidates = Array.isArray(accounts) ? accounts.filter(isAuthorizedHotmailAccount) : [];
  91. if (!candidates.length) return null;
  92. const excludeIds = new Set((options.excludeIds || []).filter(Boolean));
  93. const filtered = candidates.filter((account) => !excludeIds.has(account.id));
  94. const pool = filtered.length ? filtered : candidates;
  95. return pool
  96. .slice()
  97. .sort((left, right) => {
  98. const leftUsedAt = normalizeTimestamp(left.lastUsedAt);
  99. const rightUsedAt = normalizeTimestamp(right.lastUsedAt);
  100. if (leftUsedAt !== rightUsedAt) {
  101. return leftUsedAt - rightUsedAt;
  102. }
  103. return String(left.email || '').localeCompare(String(right.email || ''));
  104. })[0] || null;
  105. }
  106. function messageMatchesFilters(message, filters = {}) {
  107. const senderFilters = (filters.senderFilters || []).map(normalizeText).filter(Boolean);
  108. const subjectFilters = (filters.subjectFilters || []).map(normalizeText).filter(Boolean);
  109. const afterTimestamp = normalizeTimestamp(filters.afterTimestamp);
  110. const receivedAt = normalizeTimestamp(message?.receivedDateTime);
  111. if (afterTimestamp && receivedAt && receivedAt < afterTimestamp) {
  112. return null;
  113. }
  114. const sender = normalizeText(message?.from?.emailAddress?.address);
  115. const subject = normalizeText(message?.subject);
  116. const preview = String(message?.bodyPreview || '');
  117. const combinedText = [subject, sender, preview].filter(Boolean).join(' ');
  118. const code = extractVerificationCode(combinedText);
  119. const excludedCodes = new Set((filters.excludeCodes || []).filter(Boolean));
  120. if (code && excludedCodes.has(code)) {
  121. return null;
  122. }
  123. const senderMatch = senderFilters.length === 0
  124. ? true
  125. : senderFilters.some((item) => sender.includes(item) || normalizeText(preview).includes(item));
  126. const subjectMatch = subjectFilters.length === 0
  127. ? true
  128. : subjectFilters.some((item) => subject.includes(item) || normalizeText(preview).includes(item));
  129. if (!senderMatch && !subjectMatch) {
  130. return null;
  131. }
  132. if (!code) {
  133. return null;
  134. }
  135. return {
  136. code,
  137. message,
  138. receivedAt,
  139. };
  140. }
  141. function pickVerificationMessage(messages, filters = {}) {
  142. const matches = (Array.isArray(messages) ? messages : [])
  143. .map((message) => messageMatchesFilters(message, filters))
  144. .filter(Boolean)
  145. .sort((left, right) => right.receivedAt - left.receivedAt);
  146. return matches[0] || null;
  147. }
  148. function pickVerificationMessageWithFallback(messages, filters = {}) {
  149. const strictMatch = pickVerificationMessage(messages, filters);
  150. if (strictMatch) {
  151. return {
  152. match: strictMatch,
  153. usedRelaxedFilters: false,
  154. usedTimeFallback: false,
  155. };
  156. }
  157. const relaxedMatch = pickVerificationMessage(messages, {
  158. afterTimestamp: filters.afterTimestamp,
  159. excludeCodes: filters.excludeCodes,
  160. senderFilters: [],
  161. subjectFilters: [],
  162. });
  163. return {
  164. match: relaxedMatch || null,
  165. usedRelaxedFilters: Boolean(relaxedMatch),
  166. usedTimeFallback: false,
  167. };
  168. /* c8 ignore start */
  169. }
  170. function pickVerificationMessageWithTimeFallback(messages, filters = {}) {
  171. const strictOrRelaxedResult = pickVerificationMessageWithFallback(messages, filters);
  172. if (strictOrRelaxedResult.match) {
  173. return strictOrRelaxedResult;
  174. }
  175. const timeFallbackMatch = pickVerificationMessage(messages, {
  176. afterTimestamp: 0,
  177. excludeCodes: filters.excludeCodes,
  178. senderFilters: [],
  179. subjectFilters: [],
  180. });
  181. return {
  182. match: timeFallbackMatch || null,
  183. usedRelaxedFilters: Boolean(timeFallbackMatch),
  184. usedTimeFallback: Boolean(timeFallbackMatch),
  185. };
  186. /* c8 ignore stop */
  187. }
  188. function firstNonEmptyString(values) {
  189. for (const value of values) {
  190. if (value === undefined || value === null) continue;
  191. const normalized = String(value).trim();
  192. if (normalized) return normalized;
  193. }
  194. return '';
  195. }
  196. function normalizeMailAddress(rawValue) {
  197. if (!rawValue) return '';
  198. if (typeof rawValue === 'string') {
  199. return rawValue.trim();
  200. }
  201. if (typeof rawValue === 'object') {
  202. return firstNonEmptyString([
  203. rawValue.emailAddress?.address,
  204. rawValue.address,
  205. rawValue.email,
  206. rawValue.sender,
  207. rawValue.from,
  208. ]);
  209. }
  210. return '';
  211. }
  212. function stripHtmlTags(text) {
  213. return String(text || '').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
  214. }
  215. function normalizeHotmailMailApiMessage(message = {}) {
  216. return {
  217. id: firstNonEmptyString([message.id, message.message_id, message.messageId, message.internetMessageId]),
  218. subject: firstNonEmptyString([message.subject, message.title]),
  219. from: {
  220. emailAddress: {
  221. address: normalizeMailAddress(
  222. message.from_email
  223. || message.sender_email
  224. || message.from
  225. || message.sender
  226. || message.emailAddress
  227. ),
  228. },
  229. },
  230. bodyPreview: firstNonEmptyString([
  231. message.bodyPreview,
  232. message.preview,
  233. message.snippet,
  234. message.text,
  235. message.body,
  236. stripHtmlTags(message.html || message.content || ''),
  237. ]),
  238. receivedDateTime: firstNonEmptyString([
  239. message.receivedDateTime,
  240. message.received_at,
  241. message.receivedAt,
  242. message.date,
  243. message.created_at,
  244. message.time,
  245. ]),
  246. };
  247. }
  248. function normalizeHotmailMailApiMessages(messages) {
  249. const list = Array.isArray(messages)
  250. ? messages
  251. : (messages ? [messages] : []);
  252. return list.map((message) => normalizeHotmailMailApiMessage(message));
  253. }
  254. function buildHotmailMailApiLatestUrl(options) {
  255. const url = new URL(HOTMAIL_MAIL_API_URL);
  256. url.searchParams.set('refresh_token', String(options?.refreshToken || ''));
  257. url.searchParams.set('client_id', String(options?.clientId || ''));
  258. url.searchParams.set('email', String(options?.email || ''));
  259. url.searchParams.set('mailbox', String(options?.mailbox || 'INBOX'));
  260. url.searchParams.set('response_type', String(options?.responseType || 'json'));
  261. return url.toString();
  262. }
  263. function getHotmailVerificationPollConfig(step) {
  264. if (step === 4 || step === 7) {
  265. return {
  266. initialDelayMs: 5000,
  267. maxAttempts: 12,
  268. intervalMs: 5000,
  269. requestFreshCodeFirst: false,
  270. ignorePersistedLastCode: true,
  271. };
  272. }
  273. return {
  274. initialDelayMs: 5000,
  275. maxAttempts: 8,
  276. intervalMs: 4000,
  277. requestFreshCodeFirst: false,
  278. ignorePersistedLastCode: true,
  279. };
  280. }
  281. function getHotmailVerificationRequestTimestamp(step, state = {}, options = {}) {
  282. const bufferMs = Number(options.bufferMs) || 15_000;
  283. const signupRequestedAt = normalizeTimestamp(state.signupVerificationRequestedAt);
  284. const loginRequestedAt = normalizeTimestamp(state.loginVerificationRequestedAt);
  285. const lastEmailTimestamp = normalizeTimestamp(state.lastEmailTimestamp);
  286. const flowStartTime = normalizeTimestamp(state.flowStartTime);
  287. if (step === 4 && signupRequestedAt) {
  288. return Math.max(0, signupRequestedAt - bufferMs);
  289. }
  290. if (step === 7 && loginRequestedAt) {
  291. return Math.max(0, loginRequestedAt - bufferMs);
  292. }
  293. return step === 7
  294. ? (lastEmailTimestamp || flowStartTime || 0)
  295. : (flowStartTime || 0);
  296. }
  297. function getHotmailMailApiRequestConfig() {
  298. return {
  299. timeoutMs: 15000,
  300. };
  301. }
  302. function parseHotmailImportText(rawText) {
  303. const lines = String(rawText || '')
  304. .split(/\r?\n/)
  305. .map((line) => line.trim())
  306. .filter(Boolean);
  307. return lines
  308. .filter((line, index) => !(index === 0 && /^账号----密码----ID----Token$/i.test(line)))
  309. .map((line) => line.split('----').map((part) => part.trim()))
  310. .filter((parts) => parts.length >= 4 && parts[0] && parts[2])
  311. .map(([email, password, clientId, refreshToken]) => ({
  312. email,
  313. password,
  314. clientId,
  315. refreshToken,
  316. }));
  317. }
  318. return {
  319. buildHotmailMailApiLatestUrl,
  320. extractVerificationCodeFromMessage,
  321. filterHotmailAccountsByUsage,
  322. extractVerificationCode,
  323. getLatestHotmailMessage,
  324. getHotmailBulkActionLabel,
  325. getHotmailListToggleLabel,
  326. getHotmailMailApiRequestConfig,
  327. getHotmailVerificationPollConfig,
  328. getHotmailVerificationRequestTimestamp,
  329. isAuthorizedHotmailAccount,
  330. normalizeHotmailMailApiMessages,
  331. normalizeTimestamp,
  332. parseHotmailImportText,
  333. pickHotmailAccountForRun,
  334. pickVerificationMessage,
  335. pickVerificationMessageWithFallback,
  336. pickVerificationMessageWithTimeFallback,
  337. shouldClearHotmailCurrentSelection,
  338. upsertHotmailAccountInList,
  339. };
  340. });