luckmail-utils.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  1. (function luckmailUtilsModule(root, factory) {
  2. if (typeof module !== 'undefined' && module.exports) {
  3. module.exports = factory();
  4. return;
  5. }
  6. root.LuckMailUtils = factory();
  7. })(typeof self !== 'undefined' ? self : globalThis, function createLuckmailUtils() {
  8. const DEFAULT_LUCKMAIL_BASE_URL = 'https://mails.luckyous.com';
  9. const DEFAULT_LUCKMAIL_EMAIL_TYPE = 'ms_graph';
  10. const DEFAULT_LUCKMAIL_PROJECT_CODE = 'openai';
  11. const DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME = '保留';
  12. const LUCKMAIL_EMAIL_TYPES = ['self_built', 'ms_imap', 'ms_graph', 'google_variant'];
  13. function firstNonEmptyString(values) {
  14. for (const value of values) {
  15. if (value === undefined || value === null) continue;
  16. const normalized = String(value).trim();
  17. if (normalized) return normalized;
  18. }
  19. return '';
  20. }
  21. function normalizeText(value) {
  22. return String(value || '')
  23. .replace(/\s+/g, ' ')
  24. .trim()
  25. .toLowerCase();
  26. }
  27. function normalizeTimestamp(value) {
  28. if (!value) return 0;
  29. if (typeof value === 'number' && Number.isFinite(value)) {
  30. return value > 0 ? value : 0;
  31. }
  32. const rawValue = String(value || '').trim();
  33. const utcLikeMatch = rawValue.match(
  34. /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})(?::(\d{2}))?$/
  35. );
  36. if (utcLikeMatch && !/[zZ]|[+\-]\d{2}:?\d{2}$/.test(rawValue)) {
  37. const [, year, month, day, hour, minute, second = '0'] = utcLikeMatch;
  38. return Date.UTC(
  39. Number(year),
  40. Number(month) - 1,
  41. Number(day),
  42. Number(hour),
  43. Number(minute),
  44. Number(second),
  45. 0
  46. );
  47. }
  48. const timestamp = Date.parse(rawValue);
  49. return Number.isFinite(timestamp) ? timestamp : 0;
  50. }
  51. function normalizeLuckmailBaseUrl(rawValue = '') {
  52. const value = String(rawValue || '').trim();
  53. if (!value) return DEFAULT_LUCKMAIL_BASE_URL;
  54. try {
  55. const parsed = new URL(value);
  56. if (!['http:', 'https:'].includes(parsed.protocol)) {
  57. return DEFAULT_LUCKMAIL_BASE_URL;
  58. }
  59. parsed.pathname = parsed.pathname.replace(/\/+$/, '');
  60. parsed.search = '';
  61. parsed.hash = '';
  62. return parsed.toString().replace(/\/$/, '');
  63. } catch {
  64. return DEFAULT_LUCKMAIL_BASE_URL;
  65. }
  66. }
  67. function normalizeLuckmailEmailType(rawValue = '') {
  68. const normalized = String(rawValue || '').trim().toLowerCase();
  69. return LUCKMAIL_EMAIL_TYPES.includes(normalized)
  70. ? normalized
  71. : DEFAULT_LUCKMAIL_EMAIL_TYPE;
  72. }
  73. function normalizeLuckmailProjectName(rawValue = '') {
  74. return normalizeText(rawValue);
  75. }
  76. function extractLuckmailVerificationCode(text) {
  77. const source = String(text || '');
  78. const matchCn = source.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/i);
  79. if (matchCn) return matchCn[1];
  80. const matchChatGPT = source.match(/your\s+chatgpt\s+code\s+is\s+(\d{6})/i);
  81. if (matchChatGPT) return matchChatGPT[1];
  82. const matchEn = source.match(/code(?:\s+is|[\s:])+(\d{6})/i);
  83. if (matchEn) return matchEn[1];
  84. const matchStandalone = source.match(/\b(\d{6})\b/);
  85. return matchStandalone ? matchStandalone[1] : null;
  86. }
  87. function normalizeLuckmailTag(item = {}) {
  88. const safeItem = item && typeof item === 'object' ? item : {};
  89. return {
  90. id: Number(safeItem.id) || 0,
  91. name: firstNonEmptyString([safeItem.name, safeItem.tag_name]),
  92. remark: firstNonEmptyString([safeItem.remark]),
  93. limit_type: Number(safeItem.limit_type) || 0,
  94. purchase_count: Number(safeItem.purchase_count) || 0,
  95. created_at: firstNonEmptyString([safeItem.created_at]) || null,
  96. };
  97. }
  98. function normalizeLuckmailTags(input) {
  99. const list = Array.isArray(input?.list)
  100. ? input.list
  101. : (Array.isArray(input) ? input : []);
  102. return list
  103. .map((item) => normalizeLuckmailTag(item))
  104. .filter((item) => item.id > 0 || item.name);
  105. }
  106. function normalizeLuckmailPurchase(item = {}) {
  107. const safeItem = item && typeof item === 'object' ? item : {};
  108. const projectName = firstNonEmptyString([safeItem.project_name, safeItem.project]);
  109. return {
  110. id: Number(safeItem.id) || 0,
  111. email_address: firstNonEmptyString([safeItem.email_address, safeItem.address]),
  112. token: firstNonEmptyString([safeItem.token]),
  113. project_name: projectName,
  114. project_code: normalizeLuckmailProjectName(projectName),
  115. price: firstNonEmptyString([safeItem.price]) || '0.0000',
  116. status: Number(safeItem.status) || 0,
  117. tag_id: Number(safeItem.tag_id) || 0,
  118. tag_name: firstNonEmptyString([safeItem.tag_name]),
  119. user_disabled: Number(safeItem.user_disabled) || 0,
  120. warranty_hours: Number(safeItem.warranty_hours) || 0,
  121. warranty_until: firstNonEmptyString([safeItem.warranty_until]) || null,
  122. created_at: firstNonEmptyString([safeItem.created_at]) || null,
  123. };
  124. }
  125. function normalizeLuckmailPurchases(result) {
  126. const list = Array.isArray(result?.purchases)
  127. ? result.purchases
  128. : (Array.isArray(result) ? result : []);
  129. return list.map((item) => normalizeLuckmailPurchase(item));
  130. }
  131. function normalizeLuckmailPurchaseListPage(result = {}) {
  132. const safeResult = result && typeof result === 'object' ? result : {};
  133. const list = Array.isArray(safeResult.list)
  134. ? safeResult.list
  135. : (Array.isArray(safeResult.purchases) ? safeResult.purchases : []);
  136. const total = Number(safeResult.total);
  137. return {
  138. list: list.map((item) => normalizeLuckmailPurchase(item)),
  139. total: Number.isFinite(total) && total >= 0 ? total : 0,
  140. page: Math.max(1, Number(safeResult.page) || 1),
  141. page_size: Math.max(1, Number(safeResult.page_size || safeResult.pageSize) || list.length || 1),
  142. };
  143. }
  144. function normalizeLuckmailPurchaseId(value) {
  145. const numeric = Number(value);
  146. if (!Number.isFinite(numeric) || numeric <= 0) {
  147. return '';
  148. }
  149. return String(Math.floor(numeric));
  150. }
  151. function normalizeLuckmailUsedPurchases(rawValue = {}) {
  152. if (!rawValue || typeof rawValue !== 'object' || Array.isArray(rawValue)) {
  153. return {};
  154. }
  155. return Object.entries(rawValue).reduce((result, [key, value]) => {
  156. const normalizedKey = normalizeLuckmailPurchaseId(key);
  157. if (!normalizedKey) {
  158. return result;
  159. }
  160. result[normalizedKey] = Boolean(value);
  161. return result;
  162. }, {});
  163. }
  164. function isLuckmailPurchaseForProject(purchase, projectCode = DEFAULT_LUCKMAIL_PROJECT_CODE) {
  165. const normalizedPurchase = normalizeLuckmailPurchase(purchase);
  166. return normalizeLuckmailProjectName(normalizedPurchase.project_name) === normalizeLuckmailProjectName(projectCode);
  167. }
  168. function normalizeLuckmailTokenMail(mail = {}) {
  169. const safeMail = mail && typeof mail === 'object' ? mail : {};
  170. const subject = firstNonEmptyString([safeMail.subject, safeMail.title]);
  171. const body = firstNonEmptyString([safeMail.body, safeMail.body_text, safeMail.text]);
  172. const htmlBody = firstNonEmptyString([safeMail.html_body, safeMail.body_html, safeMail.html]);
  173. const from = firstNonEmptyString([safeMail.from, safeMail.sender]);
  174. const verificationCode = firstNonEmptyString([safeMail.verification_code])
  175. || extractLuckmailVerificationCode([subject, body, htmlBody, from].filter(Boolean).join(' '));
  176. return {
  177. message_id: firstNonEmptyString([safeMail.message_id, safeMail.id]),
  178. from,
  179. subject,
  180. body,
  181. html_body: htmlBody,
  182. received_at: firstNonEmptyString([safeMail.received_at, safeMail.receivedAt, safeMail.created_at]),
  183. verification_code: verificationCode || '',
  184. };
  185. }
  186. function normalizeLuckmailTokenMails(input) {
  187. const list = Array.isArray(input?.mails)
  188. ? input.mails
  189. : (Array.isArray(input) ? input : []);
  190. return list.map((mail) => normalizeLuckmailTokenMail(mail));
  191. }
  192. function normalizeLuckmailTokenCode(result = {}) {
  193. return {
  194. email_address: firstNonEmptyString([result.email_address, result.address]),
  195. project: firstNonEmptyString([result.project]),
  196. has_new_mail: Boolean(result.has_new_mail),
  197. verification_code: firstNonEmptyString([result.verification_code]) || null,
  198. mail: result.mail ? normalizeLuckmailTokenMail(result.mail) : null,
  199. };
  200. }
  201. function normalizeLuckmailMailCursor(cursor = {}) {
  202. const safeCursor = cursor && typeof cursor === 'object' ? cursor : {};
  203. return {
  204. messageId: firstNonEmptyString([safeCursor.messageId, safeCursor.message_id]),
  205. receivedAt: firstNonEmptyString([safeCursor.receivedAt, safeCursor.received_at]),
  206. };
  207. }
  208. function buildLuckmailMailCursor(mail = {}) {
  209. const normalizedMail = normalizeLuckmailTokenMail(mail);
  210. return normalizeLuckmailMailCursor({
  211. messageId: normalizedMail.message_id,
  212. receivedAt: normalizedMail.received_at,
  213. });
  214. }
  215. function buildLuckmailBaselineCursor(mails) {
  216. const latestMail = normalizeLuckmailTokenMails(mails)
  217. .sort((left, right) => {
  218. const leftTimestamp = normalizeTimestamp(left.received_at);
  219. const rightTimestamp = normalizeTimestamp(right.received_at);
  220. if (leftTimestamp !== rightTimestamp) {
  221. return rightTimestamp - leftTimestamp;
  222. }
  223. return String(right.message_id || '').localeCompare(String(left.message_id || ''));
  224. })[0] || null;
  225. return latestMail ? buildLuckmailMailCursor(latestMail) : null;
  226. }
  227. function isLuckmailMailNewerThanCursor(mail = {}, cursor = {}) {
  228. const normalizedMail = normalizeLuckmailTokenMail(mail);
  229. const normalizedCursor = normalizeLuckmailMailCursor(cursor);
  230. if (!normalizedCursor.messageId && !normalizedCursor.receivedAt) {
  231. return true;
  232. }
  233. if (normalizedMail.message_id && normalizedCursor.messageId && normalizedMail.message_id === normalizedCursor.messageId) {
  234. return false;
  235. }
  236. const mailTimestamp = normalizeTimestamp(normalizedMail.received_at);
  237. const cursorTimestamp = normalizeTimestamp(normalizedCursor.receivedAt);
  238. if (mailTimestamp && cursorTimestamp) {
  239. if (mailTimestamp > cursorTimestamp) return true;
  240. if (mailTimestamp < cursorTimestamp) return false;
  241. return Boolean(
  242. normalizedMail.message_id
  243. && normalizedCursor.messageId
  244. && normalizedMail.message_id !== normalizedCursor.messageId
  245. );
  246. }
  247. if (normalizedMail.message_id && normalizedCursor.messageId) {
  248. return normalizedMail.message_id !== normalizedCursor.messageId;
  249. }
  250. return !cursorTimestamp || Boolean(mailTimestamp && mailTimestamp > cursorTimestamp);
  251. }
  252. function isLuckmailPurchaseExpired(purchase, now = Date.now()) {
  253. const normalizedPurchase = normalizeLuckmailPurchase(purchase);
  254. const expiresAt = normalizeTimestamp(normalizedPurchase.warranty_until);
  255. return Boolean(expiresAt && expiresAt <= Number(now || 0));
  256. }
  257. function isLuckmailPurchaseDisabled(purchase) {
  258. return Number(normalizeLuckmailPurchase(purchase).user_disabled) === 1;
  259. }
  260. function isLuckmailPurchasePreserved(purchase, options = {}) {
  261. const normalizedPurchase = normalizeLuckmailPurchase(purchase);
  262. const expectedTagId = Number(options.preserveTagId) || 0;
  263. const expectedTagName = normalizeText(options.preserveTagName || DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME);
  264. if (expectedTagId > 0 && normalizedPurchase.tag_id === expectedTagId) {
  265. return true;
  266. }
  267. return Boolean(normalizedPurchase.tag_name && normalizeText(normalizedPurchase.tag_name) === expectedTagName);
  268. }
  269. function isLuckmailPurchaseReusable(purchase, options = {}) {
  270. const normalizedPurchase = normalizeLuckmailPurchase(purchase);
  271. const usedPurchases = normalizeLuckmailUsedPurchases(options.usedPurchases);
  272. const purchaseId = normalizeLuckmailPurchaseId(normalizedPurchase.id);
  273. if (!isLuckmailPurchaseForProject(normalizedPurchase, options.projectCode || DEFAULT_LUCKMAIL_PROJECT_CODE)) {
  274. return false;
  275. }
  276. if (!normalizedPurchase.email_address || !normalizedPurchase.token) {
  277. return false;
  278. }
  279. if (isLuckmailPurchaseDisabled(normalizedPurchase)) {
  280. return false;
  281. }
  282. if (purchaseId && usedPurchases[purchaseId]) {
  283. return false;
  284. }
  285. if (isLuckmailPurchasePreserved(normalizedPurchase, options)) {
  286. return false;
  287. }
  288. if (isLuckmailPurchaseExpired(normalizedPurchase, options.now || Date.now())) {
  289. return false;
  290. }
  291. return true;
  292. }
  293. function filterReusableLuckmailPurchases(purchases, options = {}) {
  294. const list = Array.isArray(purchases)
  295. ? purchases
  296. : normalizeLuckmailPurchaseListPage(purchases).list;
  297. return list
  298. .map((purchase) => normalizeLuckmailPurchase(purchase))
  299. .filter((purchase) => isLuckmailPurchaseReusable(purchase, options));
  300. }
  301. function pickReusableLuckmailPurchase(purchases, options = {}) {
  302. return filterReusableLuckmailPurchases(purchases, options)[0] || null;
  303. }
  304. function mailMatchesLuckmailFilters(mail, filters = {}) {
  305. const normalizedMail = normalizeLuckmailTokenMail(mail);
  306. const afterTimestamp = normalizeTimestamp(filters.afterTimestamp);
  307. const receivedAt = normalizeTimestamp(normalizedMail.received_at);
  308. if (afterTimestamp && receivedAt && receivedAt < afterTimestamp) {
  309. return null;
  310. }
  311. const senderFilters = (filters.senderFilters || []).map(normalizeText).filter(Boolean);
  312. const subjectFilters = (filters.subjectFilters || []).map(normalizeText).filter(Boolean);
  313. const excludedCodes = new Set((filters.excludeCodes || []).filter(Boolean));
  314. const combinedText = [
  315. normalizedMail.subject,
  316. normalizedMail.from,
  317. normalizedMail.body,
  318. normalizedMail.html_body,
  319. ].filter(Boolean).join(' ');
  320. const combinedTextNormalized = normalizeText(combinedText);
  321. const senderNormalized = normalizeText(normalizedMail.from);
  322. const subjectNormalized = normalizeText(normalizedMail.subject);
  323. const code = normalizedMail.verification_code || extractLuckmailVerificationCode(combinedText);
  324. if (!code || excludedCodes.has(code)) {
  325. return null;
  326. }
  327. const senderMatch = senderFilters.length === 0
  328. ? true
  329. : senderFilters.some((item) => senderNormalized.includes(item) || combinedTextNormalized.includes(item));
  330. const subjectMatch = subjectFilters.length === 0
  331. ? true
  332. : subjectFilters.some((item) => subjectNormalized.includes(item) || combinedTextNormalized.includes(item));
  333. if (!senderMatch && !subjectMatch) {
  334. return null;
  335. }
  336. return {
  337. code,
  338. mail: normalizedMail,
  339. receivedAt,
  340. };
  341. }
  342. function pickLuckmailVerificationMail(mails, filters = {}) {
  343. const matches = normalizeLuckmailTokenMails(mails)
  344. .map((mail) => mailMatchesLuckmailFilters(mail, filters))
  345. .filter(Boolean)
  346. .sort((left, right) => {
  347. if (left.receivedAt !== right.receivedAt) {
  348. return right.receivedAt - left.receivedAt;
  349. }
  350. return String(right.mail.message_id || '').localeCompare(String(left.mail.message_id || ''));
  351. });
  352. return matches[0] || null;
  353. }
  354. return {
  355. DEFAULT_LUCKMAIL_BASE_URL,
  356. DEFAULT_LUCKMAIL_EMAIL_TYPE,
  357. DEFAULT_LUCKMAIL_PROJECT_CODE,
  358. DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME,
  359. LUCKMAIL_EMAIL_TYPES,
  360. buildLuckmailBaselineCursor,
  361. buildLuckmailMailCursor,
  362. extractLuckmailVerificationCode,
  363. filterReusableLuckmailPurchases,
  364. isLuckmailMailNewerThanCursor,
  365. isLuckmailPurchaseDisabled,
  366. isLuckmailPurchaseExpired,
  367. isLuckmailPurchaseForProject,
  368. isLuckmailPurchasePreserved,
  369. isLuckmailPurchaseReusable,
  370. normalizeLuckmailBaseUrl,
  371. normalizeLuckmailEmailType,
  372. normalizeLuckmailMailCursor,
  373. normalizeLuckmailProjectName,
  374. normalizeLuckmailPurchase,
  375. normalizeLuckmailPurchaseId,
  376. normalizeLuckmailPurchaseListPage,
  377. normalizeLuckmailPurchases,
  378. normalizeLuckmailTag,
  379. normalizeLuckmailTags,
  380. normalizeLuckmailTokenCode,
  381. normalizeLuckmailTokenMail,
  382. normalizeLuckmailTokenMails,
  383. normalizeLuckmailUsedPurchases,
  384. normalizeText,
  385. normalizeTimestamp,
  386. pickLuckmailVerificationMail,
  387. pickReusableLuckmailPurchase,
  388. };
  389. });