hotmail-utils.js 13 KB

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