microsoft-email.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. (function attachMicrosoftEmailHelpers(globalScope) {
  2. const OPENAI_SENDER_PATTERNS = [
  3. /openai\.com/i,
  4. /auth0\.openai\.com/i,
  5. ];
  6. const CODE_PATTERN = /\b(\d{6})\b/;
  7. const TOKEN_ENDPOINT = 'https://login.microsoftonline.com/consumers/oauth2/v2.0/token';
  8. const OUTLOOK_API_BASE = 'https://outlook.office.com/api/v2.0/me/messages';
  9. async function exchangeRefreshToken(clientId, refreshToken, options = {}) {
  10. const fetchImpl = options.fetchImpl || globalScope.fetch;
  11. if (typeof fetchImpl !== 'function') {
  12. throw new Error('Microsoft 邮箱 helper 缺少 fetch 实现。');
  13. }
  14. const body = new URLSearchParams({
  15. client_id: clientId,
  16. grant_type: 'refresh_token',
  17. refresh_token: refreshToken,
  18. });
  19. const response = await fetchImpl(TOKEN_ENDPOINT, {
  20. method: 'POST',
  21. headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  22. body: body.toString(),
  23. signal: options.signal,
  24. });
  25. if (!response.ok) {
  26. const text = await response.text().catch(() => '');
  27. const errorInfo = (() => {
  28. try {
  29. return JSON.parse(text);
  30. } catch {
  31. return {};
  32. }
  33. })();
  34. throw new Error(
  35. errorInfo.error_description
  36. || `Token exchange failed (${response.status}): ${text.slice(0, 200)}`
  37. );
  38. }
  39. const data = await response.json();
  40. if (!data.access_token) {
  41. throw new Error('Token exchange response missing access_token.');
  42. }
  43. return data;
  44. }
  45. async function fetchOutlookMessages(accessToken, options = {}) {
  46. const { top = 5, signal } = options;
  47. const fetchImpl = options.fetchImpl || globalScope.fetch;
  48. if (typeof fetchImpl !== 'function') {
  49. throw new Error('Microsoft 邮箱 helper 缺少 fetch 实现。');
  50. }
  51. const response = await fetchImpl(
  52. `${OUTLOOK_API_BASE}?$top=${encodeURIComponent(top)}&$orderby=ReceivedDateTime desc&$select=From,Subject,ReceivedDateTime,BodyPreview,Body`,
  53. {
  54. method: 'GET',
  55. headers: { Authorization: `Bearer ${accessToken}` },
  56. signal,
  57. }
  58. );
  59. if (response.status === 401 || response.status === 403) {
  60. throw new Error('Microsoft Graph token invalid or expired.');
  61. }
  62. if (!response.ok) {
  63. const body = await response.text().catch(() => '');
  64. throw new Error(`Outlook API request failed (${response.status}): ${body || response.statusText}`);
  65. }
  66. const payload = await response.json();
  67. return Array.isArray(payload?.value) ? payload.value : [];
  68. }
  69. function normalizeMessage(message) {
  70. return {
  71. from: {
  72. emailAddress: {
  73. address: message?.From?.EmailAddress?.Address
  74. || message?.from?.emailAddress?.address
  75. || '',
  76. },
  77. },
  78. subject: message?.Subject || message?.subject || '',
  79. receivedDateTime: message?.ReceivedDateTime || message?.receivedDateTime || '',
  80. bodyPreview: message?.BodyPreview || message?.bodyPreview || '',
  81. body: {
  82. content: message?.Body?.Content || message?.body?.content || '',
  83. },
  84. id: message?.Id || message?.id || '',
  85. };
  86. }
  87. function getMessageSender(message) {
  88. return String(
  89. message?.from?.emailAddress?.address
  90. || message?.sender?.emailAddress?.address
  91. || ''
  92. ).trim();
  93. }
  94. function getMessageTimestamp(message) {
  95. const value = Date.parse(message?.receivedDateTime || message?.createdDateTime || '');
  96. return Number.isFinite(value) ? value : 0;
  97. }
  98. function getMessageSearchText(message) {
  99. return [
  100. message?.subject,
  101. message?.bodyPreview,
  102. message?.body?.content,
  103. getMessageSender(message),
  104. ]
  105. .map((value) => String(value || ''))
  106. .join('\n');
  107. }
  108. function isOpenAiMessage(message) {
  109. const sender = getMessageSender(message);
  110. if (OPENAI_SENDER_PATTERNS.some((pattern) => pattern.test(sender))) {
  111. return true;
  112. }
  113. const searchText = getMessageSearchText(message);
  114. return OPENAI_SENDER_PATTERNS.some((pattern) => pattern.test(searchText));
  115. }
  116. function extractVerificationCodeFromMessages(messages, options = {}) {
  117. const { filterAfterTimestamp = 0 } = options;
  118. for (const raw of messages) {
  119. const message = normalizeMessage(raw);
  120. const receivedAt = getMessageTimestamp(message);
  121. if (receivedAt && receivedAt < Number(filterAfterTimestamp || 0)) {
  122. continue;
  123. }
  124. if (!isOpenAiMessage(message)) {
  125. continue;
  126. }
  127. const match = getMessageSearchText(message).match(CODE_PATTERN);
  128. if (!match) {
  129. continue;
  130. }
  131. return {
  132. code: match[1],
  133. emailTimestamp: receivedAt || Date.now(),
  134. messageId: message?.id || null,
  135. sender: getMessageSender(message),
  136. subject: String(message?.subject || ''),
  137. };
  138. }
  139. return null;
  140. }
  141. async function fetchMicrosoftMailboxMessages(options = {}) {
  142. const {
  143. clientId,
  144. refreshToken,
  145. top = 5,
  146. fetchImpl,
  147. signal,
  148. } = options;
  149. if (!refreshToken) {
  150. throw new Error('Microsoft refresh token is empty.');
  151. }
  152. if (!clientId) {
  153. throw new Error('Microsoft client_id is empty.');
  154. }
  155. const tokenData = await exchangeRefreshToken(clientId, refreshToken, { fetchImpl, signal });
  156. const rawMessages = await fetchOutlookMessages(tokenData.access_token, { top, signal, fetchImpl });
  157. return {
  158. tokenData,
  159. nextRefreshToken: String(tokenData?.refresh_token || '').trim(),
  160. messages: rawMessages.map((message) => normalizeMessage(message)),
  161. };
  162. }
  163. async function fetchMicrosoftVerificationCode(options = {}) {
  164. const {
  165. token,
  166. clientId,
  167. maxRetries = 3,
  168. retryDelayMs = 10000,
  169. log = null,
  170. filterAfterTimestamp = 0,
  171. fetchImpl,
  172. signal,
  173. } = options;
  174. if (!token) {
  175. throw new Error('Microsoft refresh token is empty.');
  176. }
  177. if (!clientId) {
  178. throw new Error('Microsoft client_id is empty.');
  179. }
  180. const tokenData = await exchangeRefreshToken(clientId, token, { fetchImpl, signal });
  181. const accessToken = tokenData.access_token;
  182. const nextRefreshToken = String(tokenData?.refresh_token || '').trim();
  183. for (let attempt = 1; attempt <= maxRetries; attempt += 1) {
  184. const messages = await fetchOutlookMessages(accessToken, { top: 5, signal, fetchImpl });
  185. const result = extractVerificationCodeFromMessages(messages, { filterAfterTimestamp });
  186. if (result) {
  187. return {
  188. ...result,
  189. nextRefreshToken,
  190. messages: messages.map((message) => normalizeMessage(message)),
  191. };
  192. }
  193. if (attempt < maxRetries) {
  194. if (typeof log === 'function') {
  195. log(`Outlook API: attempt ${attempt}/${maxRetries} found no OpenAI verification mail, retrying...`);
  196. }
  197. await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
  198. }
  199. }
  200. throw new Error('No matching OpenAI verification email found.');
  201. }
  202. const api = {
  203. CODE_PATTERN,
  204. exchangeRefreshToken,
  205. extractVerificationCodeFromMessages,
  206. fetchMicrosoftMailboxMessages,
  207. fetchMicrosoftVerificationCode,
  208. fetchOutlookMessages,
  209. getMessageSender,
  210. getMessageTimestamp,
  211. isOpenAiMessage,
  212. normalizeMessage,
  213. };
  214. globalScope.MultiPageMicrosoftEmail = api;
  215. if (typeof module !== 'undefined' && module.exports) {
  216. module.exports = api;
  217. }
  218. })(typeof globalThis !== 'undefined' ? globalThis : this);