hotmail-utils.js 12 KB

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