github-actions[bot] commited on
Commit
6c6d16c
·
1 Parent(s): 9657167

Update from GitHub Actions

Browse files
functions/api/mail/auth.ts CHANGED
@@ -1,14 +1,11 @@
1
  import { authApiToken, authMiddleware } from "../../utils/auth.js";
2
  import { addCorsHeaders } from "../../utils/cors.js";
3
- import { chromium } from 'playwright';
4
- import { getVerificationCode } from '../../utils/emailVerification.js';
5
-
6
 
7
  export const onRequest = async (context: RouteContext): Promise<Response> => {
8
  const request = context.request;
9
  const env: Env = context.env;
10
 
11
- // 验证权限
12
  const authResponse = await authMiddleware(request, env);
13
  const apiResponse = await authApiToken(request, env);
14
  if (authResponse && apiResponse) {
@@ -21,196 +18,20 @@ export const onRequest = async (context: RouteContext): Promise<Response> => {
21
  throw new Error("Email is required");
22
  }
23
 
24
- // 获取账户信息
25
- const accountsStr = await env.KV.get("accounts");
26
- const accounts: any[] = accountsStr ? JSON.parse(accountsStr) : [];
27
- const account = accounts.find(a => a.email === email);
28
- if (!account) {
29
- throw new Error("Account not found");
30
- }
31
- const clientId = env.ENTRA_CLIENT_ID;
32
- const clientSecret = env.ENTRA_CLIENT_SECRET;
33
- const redirectUri = env.AUTH_REDIRECT_URI;
34
- let browser;
35
- let context;
36
- try {
37
- browser = await chromium.launch({
38
- headless: process.env.NODE_ENV === 'development' ? false : true,
39
- args: [
40
- '--no-sandbox',
41
- '--disable-setuid-sandbox',
42
- '--disable-dev-shm-usage',
43
- '--disable-blink-features=AutomationControlled', // 禁用自动化特征
44
- '--disable-infobars',
45
- '--window-size=1920,1080'
46
- ],
47
- executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH, // 使用系统 Chromium
48
- });
49
- context = await browser.newContext();
50
- const page = await context.newPage();
51
-
52
- const authUrl = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?` +
53
- `client_id=${clientId}` +
54
- `&response_type=code` +
55
- `&redirect_uri=${encodeURIComponent(redirectUri)}` +
56
- `&response_mode=query` +
57
- `&scope=offline_access%20IMAP.AccessAsUser.All%20User.Read%20Mail.ReadWrite.Shared%20Mail.Send%20Mail.Read` +
58
- `&prompt=consent` + //强制显示权限确认窗口
59
- `&state=${email}`;
60
-
61
-
62
- console.log(authUrl)
63
-
64
- //开始认证
65
- await page.goto(authUrl);
66
- await page.fill('input[type="email"]', account.email);
67
- await page.click('input[type="submit"]');
68
-
69
- try {
70
- await page.waitForSelector('#idA_PWD_SwitchToPassword', { timeout: 3000 });
71
- await page.click('#idA_PWD_SwitchToPassword');
72
- } catch (error) {
73
- console.log(`没有切换到密码登录,继续执行: ${error}`);
74
- }
75
-
76
-
77
- //输入密码
78
- await page.waitForURL("https://login.live.com/**")
79
- await page.fill('input[type="password"]', account.password);
80
- await page.click('button[type="submit"]');
81
-
82
-
83
- try {
84
- await page.waitForURL("https://account.live.com/recover/**", { timeout: 5000 })
85
- await page.click('#iLandingViewAction');
86
- } catch (error) {
87
- // 如果超时或页面未出现,直接继续执行
88
- console.log("验证确定 page not found or timeout, skipping...");
89
- }
90
-
91
- for (let i = 0; i < 2; i++) {
92
- try {
93
- await page.waitForURL('https://account.live.com/identity/**', { timeout: 5000 });
94
- const proofEmail = account.proofEmail;
95
- if (!proofEmail) {
96
- throw new Error("No proof email provided");
97
- }
98
- const timestamp = Math.floor(Date.now() / 1000);
99
- try {
100
- await page.waitForSelector('#iProof0', { timeout: 3000 });
101
- await page.click('#iProof0')
102
- } catch (error) {
103
- console.log(`没有#iProof0,继续执行: ${error}`);
104
- }
105
- await page.fill("#iProofEmail", proofEmail)
106
- await page.click('input[type="submit"]')
107
-
108
- //处理email验证
109
- const proof = [
110
- {
111
- "suffix": "godgodgame.com",
112
- "apiUrl": "https://seedmail.igiven.com/api/latest-email",
113
- "token": env.PROOF_GODGODGAME_TOKEN
114
- },
115
- {
116
- "suffix": "igiven.com",
117
- "apiUrl": "https://mail.igiven.com/api/latest-email",
118
- "token": env.PROOF_IGIVEN_TOKEN
119
- }
120
- ]
121
-
122
- const suffix = proofEmail.substring(proofEmail.indexOf('@') + 1);
123
- const proofConfig = proof.find(p => p.suffix === suffix)!;
124
- console.log(suffix, proofConfig)
125
- const verificationCode = await getVerificationCode(proofConfig.apiUrl, proofConfig.token!, account.proofEmail, timestamp);
126
- console.log(`verificationCode:${verificationCode}`)
127
- await page.fill('input[type="tel"]', verificationCode);
128
- await page.click('input[type="submit"]');
129
- } catch (error) {
130
- console.log(account.email, `没有多重验证,继续执行: ${error}`);
131
- }
132
- }
133
-
134
 
135
- //确认登录
136
- await page.waitForURL((url) => {
137
- return url.href.startsWith("https://login.live.com");
138
- })
139
- await page.click('button[type="submit"]#acceptButton'); // 同意按钮
140
-
141
- //同意授权
142
- try {
143
- // 等待同意授权页面,如果超时5秒则跳过
144
- await page.waitForURL("https://account.live.com/Consent/**", { timeout: 10000 })
145
- await page.click('button[type="submit"][data-testid="appConsentPrimaryButton"]');
146
- } catch (error) {
147
- // 如果超时或页面未出现,直接继续执行
148
- console.log("Consent page not found or timeout, skipping...");
149
- }
150
- await page.waitForURL((url) => {
151
- return url.href.startsWith(redirectUri);
152
- })
153
- } finally {
154
- if (context) await context.close();
155
- if (browser) await browser.close();
156
  }
157
 
158
-
159
- // 等待回调处理完成,检查 KV 中是否有对应的 code
160
- let code = null;
161
- const maxRetries = 30;
162
- let retries = 0;
163
-
164
- while (!code && retries < maxRetries) {
165
- const codeKey = `code_${email}`;
166
- code = await env.KV.get(codeKey);
167
-
168
- if (!code) {
169
- await new Promise(resolve => setTimeout(resolve, 1000));
170
- retries++;
171
  }
172
- }
173
-
174
- if (!code) {
175
- throw new Error("Authorization timeout");
176
- }
177
-
178
- // 使用授权码获取刷新令牌
179
- const tokenResponse = await fetch('https://login.microsoftonline.com/common/oauth2/v2.0/token', {
180
- method: 'POST',
181
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
182
- body: new URLSearchParams({
183
- client_id: clientId,
184
- client_secret: clientSecret,
185
- code: code,
186
- redirect_uri: redirectUri,
187
- grant_type: 'authorization_code'
188
- })
189
- });
190
-
191
- const tokenData: any = await tokenResponse.json();
192
- if (!tokenData.refresh_token) {
193
- throw new Error("Failed to get refresh token");
194
- }
195
- if (!tokenData.expires_in) {
196
- throw new Error("Missing expires_in in token response");
197
- }
198
-
199
- // 存储刷新令牌和时间戳
200
- const tokenInfo = {
201
- ...tokenData,
202
- timestamp: Date.now()
203
- };
204
- console.log(tokenInfo);
205
- await env.KV.put(`refresh_token_${email}`, JSON.stringify(tokenInfo));
206
-
207
- // 删除临时授权码
208
- await env.KV.delete(`code_${email}`);
209
-
210
- return new Response(JSON.stringify({ success: true }), {
211
- status: 200,
212
- headers: { 'Content-Type': 'application/json' }
213
- });
214
 
215
  } catch (error: any) {
216
  return new Response(
@@ -218,4 +39,4 @@ export const onRequest = async (context: RouteContext): Promise<Response> => {
218
  { status: 500 }
219
  );
220
  }
221
- };
 
1
  import { authApiToken, authMiddleware } from "../../utils/auth.js";
2
  import { addCorsHeaders } from "../../utils/cors.js";
3
+ import { AuthService } from "../../utils/authService.js";
 
 
4
 
5
  export const onRequest = async (context: RouteContext): Promise<Response> => {
6
  const request = context.request;
7
  const env: Env = context.env;
8
 
 
9
  const authResponse = await authMiddleware(request, env);
10
  const apiResponse = await authApiToken(request, env);
11
  if (authResponse && apiResponse) {
 
18
  throw new Error("Email is required");
19
  }
20
 
21
+ const authService = new AuthService(env);
22
+ const result = await authService.authenticateEmail(email);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
+ if (!result.success) {
25
+ throw new Error(result.error);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  }
27
 
28
+ return new Response(
29
+ JSON.stringify({ success: true }),
30
+ {
31
+ status: 200,
32
+ headers: { 'Content-Type': 'application/json' }
 
 
 
 
 
 
 
 
33
  }
34
+ );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
  } catch (error: any) {
37
  return new Response(
 
39
  { status: 500 }
40
  );
41
  }
42
+ };
functions/api/mail/batch.ts ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { authApiToken, authMiddleware } from "../../utils/auth.js";
2
+ import { addCorsHeaders } from "../../utils/cors.js";
3
+ import { AuthService } from "../../utils/authService.js";
4
+
5
+ // 并发处理函数
6
+ async function processBatch<T>(
7
+ items: T[],
8
+ processor: (item: T) => Promise<any>,
9
+ concurrency: number = 2
10
+ ): Promise<any[]> {
11
+ // 创建一个队列来存储所有的 promise
12
+ const queue: Promise<any>[] = [];
13
+ const results: any[] = new Array(items.length);
14
+ let nextIndex = 0;
15
+
16
+ // 创建指定数量的worker
17
+ const workers = new Array(concurrency).fill(null).map(async () => {
18
+ while (nextIndex < items.length) {
19
+ const currentIndex = nextIndex++;
20
+ try {
21
+ const result = await processor(items[currentIndex]);
22
+ results[currentIndex] = result;
23
+ } catch (error) {
24
+ results[currentIndex] = {
25
+ error: error instanceof Error ? error.message : 'Unknown error'
26
+ };
27
+ }
28
+ }
29
+ });
30
+
31
+ // 等待所有worker完成
32
+ await Promise.all(workers);
33
+ return results;
34
+ }
35
+
36
+ // 检查认证状态和时间戳
37
+ async function checkAuthStatus(email: string, env: Env): Promise<{ needsAuth: boolean, reason?: string }> {
38
+ const tokenInfoStr = await env.KV.get(`refresh_token_${email}`);
39
+ if (!tokenInfoStr) {
40
+ return { needsAuth: true, reason: "未认证" };
41
+ }
42
+
43
+ const tokenInfo = JSON.parse(tokenInfoStr);
44
+ const threeMonths = 90 * 24 * 60 * 60 * 1000; // 90天转换为毫秒
45
+ const isExpired = Date.now() - tokenInfo.timestamp > threeMonths;
46
+
47
+ if (isExpired) {
48
+ return { needsAuth: true, reason: "认证已过期" };
49
+ }
50
+
51
+ return { needsAuth: false };
52
+ }
53
+
54
+ export const onRequest = async (context: RouteContext): Promise<Response> => {
55
+ const request = context.request;
56
+ const env: Env = context.env;
57
+
58
+ const authResponse = await authMiddleware(request, env);
59
+ const apiResponse = await authApiToken(request, env);
60
+ if (authResponse && apiResponse) {
61
+ return addCorsHeaders(authResponse);
62
+ }
63
+
64
+ try {
65
+ const { emails } = await request.json() as { emails: string[] };
66
+ if (!emails || !Array.isArray(emails)) {
67
+ throw new Error("Emails array is required");
68
+ }
69
+
70
+ const authService = new AuthService(env);
71
+ const results = await processBatch(
72
+ emails,
73
+ async (email) => {
74
+ try {
75
+ // 检查认证状态
76
+ const authStatus = await checkAuthStatus(email, env);
77
+ if (!authStatus.needsAuth) {
78
+ return {
79
+ email,
80
+ success: true,
81
+ message: "已认证且在有效期内"
82
+ };
83
+ }
84
+
85
+ // 需要重新认证
86
+ const result = await authService.authenticateEmail(email);
87
+ return {
88
+ email,
89
+ success: result.success,
90
+ message: authStatus.reason,
91
+ error: result.error
92
+ };
93
+ } catch (error: any) {
94
+ return {
95
+ email,
96
+ success: false,
97
+ error: error.message || 'Failed to process'
98
+ };
99
+ }
100
+ },
101
+ 2
102
+ );
103
+
104
+ return new Response(
105
+ JSON.stringify(results),
106
+ {
107
+ status: 200,
108
+ headers: { 'Content-Type': 'application/json' }
109
+ }
110
+ );
111
+
112
+ } catch (error: any) {
113
+ return new Response(
114
+ JSON.stringify({ error: error.message }),
115
+ { status: 500 }
116
+ );
117
+ }
118
+ };
functions/api/mail/status.ts ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { authApiToken, authMiddleware } from "../../utils/auth.js";
2
+ import { addCorsHeaders } from "../../utils/cors.js";
3
+
4
+ export const onRequest = async (context: RouteContext): Promise<Response> => {
5
+ const request = context.request;
6
+ const env: Env = context.env;
7
+
8
+ // 验证权限
9
+ const authResponse = await authMiddleware(request, env);
10
+ const apiResponse = await authApiToken(request, env);
11
+ if (authResponse && apiResponse) {
12
+ return addCorsHeaders(authResponse);
13
+ }
14
+
15
+ try {
16
+ const { email } = await request.json() as any;
17
+ if (!email) {
18
+ throw new Error("Email is required");
19
+ }
20
+
21
+ // 获取刷新令牌信息
22
+ const tokenInfoStr = await env.KV.get(`refresh_token_${email}`);
23
+ if (!tokenInfoStr) {
24
+ return new Response(
25
+ JSON.stringify({
26
+ authenticated: false,
27
+ message: "未认证"
28
+ }),
29
+ {
30
+ status: 200,
31
+ headers: {
32
+ 'Content-Type': 'application/json',
33
+ 'Access-Control-Allow-Origin': '*'
34
+ }
35
+ }
36
+ );
37
+ }
38
+
39
+ const tokenInfo = JSON.parse(tokenInfoStr);
40
+
41
+
42
+ return new Response(
43
+ JSON.stringify({
44
+ authenticated: true,
45
+ message: "已认证",
46
+ timestamp: tokenInfo.timestamp
47
+ }),
48
+ {
49
+ status: 200,
50
+ headers: {
51
+ 'Content-Type': 'application/json',
52
+ 'Access-Control-Allow-Origin': '*'
53
+ }
54
+ }
55
+ );
56
+
57
+ } catch (error: any) {
58
+ return new Response(
59
+ JSON.stringify({ error: error.message }),
60
+ {
61
+ status: 500,
62
+ headers: {
63
+ 'Content-Type': 'application/json',
64
+ 'Access-Control-Allow-Origin': '*'
65
+ }
66
+ }
67
+ );
68
+ }
69
+ };
functions/utils/authService.ts ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Page } from 'playwright';
2
+ import BrowserManager from './browser';
3
+ import { getVerificationCode } from './emailVerification.js';
4
+
5
+ interface Account {
6
+ email: string;
7
+ password: string;
8
+ proofEmail: string;
9
+ }
10
+
11
+
12
+ export class AuthService {
13
+ constructor(private env: Env) { }
14
+
15
+ async authenticateEmail(email: string): Promise<{ success: boolean; error?: string }> {
16
+ try {
17
+ const accountsStr = await this.env.KV.get("accounts");
18
+ const accounts: Account[] = accountsStr ? JSON.parse(accountsStr) : [];
19
+ const account = accounts.find(a => a.email === email);
20
+
21
+ if (!account) {
22
+ throw new Error("Account not found");
23
+ }
24
+
25
+ await this.performAuthentication(account);
26
+ await this.handleAuthorizationCallback(email);
27
+
28
+ return { success: true };
29
+ } catch (error: any) {
30
+ return {
31
+ success: false,
32
+ error: error.message
33
+ };
34
+ }
35
+ }
36
+
37
+ private async performAuthentication(account: Account) {
38
+ const clientId = this.env.ENTRA_CLIENT_ID;
39
+ const redirectUri = this.env.AUTH_REDIRECT_URI;
40
+ let browser;
41
+ let context;
42
+
43
+ try {
44
+ browser = await BrowserManager.getInstance();
45
+ context = await browser.newContext();
46
+ const page = await context.newPage();
47
+
48
+ const authUrl = this.buildAuthUrl(clientId, redirectUri, account.email);
49
+ await this.handleLoginProcess(page, account, authUrl);
50
+ await this.handleMultiFactorAuth(page, account);
51
+ await this.handleConsent(page, redirectUri);
52
+ } finally {
53
+ if (context) await context.close();
54
+ }
55
+ }
56
+
57
+ private buildAuthUrl(clientId: string, redirectUri: string, email: string): string {
58
+ return `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?` +
59
+ `client_id=${clientId}` +
60
+ `&response_type=code` +
61
+ `&redirect_uri=${encodeURIComponent(redirectUri)}` +
62
+ `&response_mode=query` +
63
+ `&scope=offline_access%20IMAP.AccessAsUser.All%20User.Read%20Mail.ReadWrite.Shared%20Mail.Send%20Mail.Read` +
64
+ `&prompt=consent` +
65
+ `&state=${email}`;
66
+ }
67
+
68
+ private async handleLoginProcess(page: Page, account: Account, authUrl: string) {
69
+ await page.goto(authUrl);
70
+ await page.fill('input[type="email"]', account.email);
71
+ await page.click('input[type="submit"]');
72
+
73
+ try {
74
+ await page.waitForSelector('#idA_PWD_SwitchToPassword', { timeout: 3000 });
75
+ await page.click('#idA_PWD_SwitchToPassword');
76
+ } catch (error) {
77
+ console.log(`没有切换到密码登录,继续执行: ${error}`);
78
+ }
79
+
80
+ await page.waitForURL("https://login.live.com/**");
81
+ await page.fill('input[type="password"]', account.password);
82
+ await page.fill('input[type="password"]', account.password);
83
+ await page.click('button[type="submit"]');
84
+
85
+ try {
86
+ await page.waitForURL("https://account.live.com/recover/**", { timeout: 5000 });
87
+ await page.click('#iLandingViewAction');
88
+ } catch (error) {
89
+ console.log("验证确定 page not found or timeout, skipping...");
90
+ }
91
+ }
92
+
93
+ private async handleMultiFactorAuth(page: Page, account: Account) {
94
+ for (let i = 0; i < 2; i++) {
95
+ try {
96
+ await page.waitForURL('https://account.live.com/identity/**', { timeout: 5000 });
97
+ const proofEmail = account.proofEmail;
98
+ if (!proofEmail) {
99
+ throw new Error("No proof email provided");
100
+ }
101
+
102
+ const timestamp = Math.floor(Date.now() / 1000);
103
+ try {
104
+ await page.waitForSelector('#iProof0', { timeout: 3000 });
105
+ await page.click('#iProof0');
106
+ } catch (error) {
107
+ console.log(`没有#iProof0,继续执行: ${error}`);
108
+ }
109
+
110
+ await page.fill("#iProofEmail", proofEmail);
111
+ await page.click('input[type="submit"]');
112
+
113
+ const proof = [
114
+ {
115
+ "suffix": "godgodgame.com",
116
+ "apiUrl": "https://seedmail.igiven.com/api/latest-email",
117
+ "token": this.env.PROOF_GODGODGAME_TOKEN
118
+ },
119
+ {
120
+ "suffix": "igiven.com",
121
+ "apiUrl": "https://mail.igiven.com/api/latest-email",
122
+ "token": this.env.PROOF_IGIVEN_TOKEN
123
+ }
124
+ ];
125
+
126
+ const suffix = proofEmail.substring(proofEmail.indexOf('@') + 1);
127
+ const proofConfig = proof.find(p => p.suffix === suffix)!;
128
+ const verificationCode = await getVerificationCode(
129
+ proofConfig.apiUrl,
130
+ proofConfig.token!,
131
+ proofEmail,
132
+ timestamp
133
+ );
134
+
135
+ await page.fill('input[type="tel"]', verificationCode);
136
+ await page.click('input[type="submit"]');
137
+ } catch (error) {
138
+ console.log(account.email, `没有多重验证,继续执行: ${error}`);
139
+ }
140
+ }
141
+ }
142
+
143
+ private async handleConsent(page: Page, redirectUri: string) {
144
+ await page.waitForURL((url: any) => url.href.startsWith("https://login.live.com"));
145
+ await page.click('button[type="submit"]#acceptButton');
146
+
147
+ try {
148
+ await page.waitForURL("https://account.live.com/Consent/**", { timeout: 10000 });
149
+ await page.click('button[type="submit"][data-testid="appConsentPrimaryButton"]');
150
+ } catch (error) {
151
+ console.log("Consent page not found or timeout, skipping...");
152
+ }
153
+
154
+ await page.waitForURL((url: any) => url.href.startsWith(redirectUri));
155
+ }
156
+
157
+ private async handleAuthorizationCallback(email: string) {
158
+ let code = null;
159
+ const maxRetries = 30;
160
+ let retries = 0;
161
+
162
+ while (!code && retries < maxRetries) {
163
+ const codeKey = `code_${email}`;
164
+ code = await this.env.KV.get(codeKey);
165
+
166
+ if (!code) {
167
+ await new Promise(resolve => setTimeout(resolve, 1000));
168
+ retries++;
169
+ }
170
+ }
171
+
172
+ if (!code) {
173
+ throw new Error("Authorization timeout");
174
+ }
175
+
176
+ const tokenResponse = await fetch('https://login.microsoftonline.com/common/oauth2/v2.0/token', {
177
+ method: 'POST',
178
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
179
+ body: new URLSearchParams({
180
+ client_id: this.env.ENTRA_CLIENT_ID,
181
+ client_secret: this.env.ENTRA_CLIENT_SECRET,
182
+ code: code,
183
+ redirect_uri: this.env.AUTH_REDIRECT_URI,
184
+ grant_type: 'authorization_code'
185
+ })
186
+ });
187
+
188
+ const tokenData: any = await tokenResponse.json();
189
+ if (!tokenData.refresh_token) {
190
+ throw new Error("Failed to get refresh token");
191
+ }
192
+ if (!tokenData.expires_in) {
193
+ throw new Error("Missing expires_in in token response");
194
+ }
195
+
196
+ const tokenInfo = {
197
+ ...tokenData,
198
+ timestamp: Date.now()
199
+ };
200
+
201
+ await this.env.KV.put(`refresh_token_${email}`, JSON.stringify(tokenInfo));
202
+ await this.env.KV.delete(`code_${email}`);
203
+ }
204
+ }
functions/utils/browser.ts ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Browser, chromium } from 'playwright';
2
+
3
+ class BrowserManager {
4
+ private static instance: Browser | null = null;
5
+ private static isInitializing = false;
6
+ private static initPromise: Promise<Browser> | null = null;
7
+
8
+ static async getInstance(): Promise<Browser> {
9
+ if (this.instance && this.instance.isConnected()) {
10
+ return this.instance;
11
+ }
12
+
13
+ if (this.isInitializing) {
14
+ return this.initPromise!;
15
+ }
16
+
17
+ this.isInitializing = true;
18
+ this.initPromise = this.initializeBrowser();
19
+
20
+ try {
21
+ this.instance = await this.initPromise;
22
+ return this.instance;
23
+ } finally {
24
+ this.isInitializing = false;
25
+ this.initPromise = null;
26
+ }
27
+ }
28
+
29
+ private static async initializeBrowser(): Promise<Browser> {
30
+ return chromium.launch({
31
+ headless: process.env.NODE_ENV === 'development' ? false : true,
32
+ args: [
33
+ '--no-sandbox',
34
+ '--disable-setuid-sandbox',
35
+ '--disable-dev-shm-usage',
36
+ '--disable-blink-features=AutomationControlled',
37
+ '--disable-infobars',
38
+ '--window-size=1920,1080'
39
+ ],
40
+ executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH,
41
+ });
42
+ }
43
+
44
+ static async closeBrowser() {
45
+ if (this.instance) {
46
+ await this.instance.close();
47
+ this.instance = null;
48
+ }
49
+ }
50
+ }
51
+
52
+ export default BrowserManager;
functions/utils/emailVerification.ts CHANGED
@@ -1,5 +1,6 @@
1
  export async function getVerificationCode(proofApi: string, apiKey: String, proofEmail: string, timestamp: number): Promise<string> {
2
  const maxRetries = 30;
 
3
  console.log(`开始获取验证码${proofApi},${apiKey},${timestamp},${new Date(timestamp * 1000)}`);
4
  for (let i = 0; i < maxRetries; i++) {
5
  try {
 
1
  export async function getVerificationCode(proofApi: string, apiKey: String, proofEmail: string, timestamp: number): Promise<string> {
2
  const maxRetries = 30;
3
+ timestamp = timestamp - 5;
4
  console.log(`开始获取验证码${proofApi},${apiKey},${timestamp},${new Date(timestamp * 1000)}`);
5
  for (let i = 0; i < maxRetries; i++) {
6
  try {
index.ts CHANGED
@@ -22,7 +22,8 @@ import { onRequest as handleMailCallback } from './functions/api/mail/callback.j
22
  import { onRequest as handleMailAll } from './functions/api/mail/all.js'
23
  import { onRequest as handleMailNew } from './functions/api/mail/new.js'
24
  import { onRequest as handleMailSend } from './functions/api/mail/send.js'
25
-
 
26
  dotenv.config({ path: ['.env', '.env.local'], override: true });
27
  const isDev = process.env.NODE_ENV === 'development'
28
 
@@ -55,7 +56,7 @@ var kv: KVNamespace = {
55
  put: async (key: string, value: string) => {
56
  await storage.setItem(key, value);
57
  },
58
- delete:async(key:string)=>{
59
  await storage.removeItem(key);
60
  }
61
  };
@@ -147,6 +148,12 @@ app.all('/api/*', async (c) => {
147
  case '/api/mail/send':
148
  response = await handleMailSend(context);
149
  break;
 
 
 
 
 
 
150
  default:
151
  return c.json({ error: 'Route not found' }, 404);
152
  }
 
22
  import { onRequest as handleMailAll } from './functions/api/mail/all.js'
23
  import { onRequest as handleMailNew } from './functions/api/mail/new.js'
24
  import { onRequest as handleMailSend } from './functions/api/mail/send.js'
25
+ import { onRequest as handleBatch } from './functions/api/mail/batch.js'
26
+ import { onRequest as handleMailStatus } from './functions/api/mail/status.js'
27
  dotenv.config({ path: ['.env', '.env.local'], override: true });
28
  const isDev = process.env.NODE_ENV === 'development'
29
 
 
56
  put: async (key: string, value: string) => {
57
  await storage.setItem(key, value);
58
  },
59
+ delete: async (key: string) => {
60
  await storage.removeItem(key);
61
  }
62
  };
 
148
  case '/api/mail/send':
149
  response = await handleMailSend(context);
150
  break;
151
+ case '/api/mail/status':
152
+ response = await handleMailStatus(context);
153
+ break;
154
+ case '/api/mail/batch':
155
+ response = await handleBatch(context);
156
+ break;
157
  default:
158
  return c.json({ error: 'Route not found' }, 404);
159
  }
src/services/mailApi.ts CHANGED
@@ -1,6 +1,31 @@
1
  import { API_BASE_URL, getHeaders, handleResponse } from './util';
2
 
3
  export const mailApi = {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  async refreshAuth(email: string) {
5
  const response = await fetch(
6
  `${API_BASE_URL}/api/mail/auth`,
 
1
  import { API_BASE_URL, getHeaders, handleResponse } from './util';
2
 
3
  export const mailApi = {
4
+ async getAuthStatus(email: string) {
5
+ const response = await fetch(
6
+ `${API_BASE_URL}/api/mail/status`,
7
+ {
8
+ headers: getHeaders(),
9
+ method: 'POST',
10
+ body: JSON.stringify({ email })
11
+ }
12
+ );
13
+ return handleResponse(response);
14
+ },
15
+
16
+ // Add new batch refresh methods
17
+ async refreshAuthBatch(emails: string[]) {
18
+ const response = await fetch(
19
+ `${API_BASE_URL}/api/mail/batch`,
20
+ {
21
+ headers: getHeaders(),
22
+ method: 'POST',
23
+ body: JSON.stringify({ emails })
24
+ }
25
+ );
26
+ return handleResponse(response);
27
+ },
28
+
29
  async refreshAuth(email: string) {
30
  const response = await fetch(
31
  `${API_BASE_URL}/api/mail/auth`,
src/views/MailView.vue CHANGED
@@ -28,9 +28,9 @@ const columns = [
28
  }
29
  ];
30
 
31
-
32
  // 添加下拉菜单选项函数
33
  const actionOptions = (row: Account) => [
 
34
  { content: '刷新认证', value: 'refresh', onClick: () => handleRefreshAuth(row) },
35
  { content: '最新邮件', value: 'latest', onClick: () => handleLatestMails(row) },
36
  { content: '所有邮件', value: 'all', onClick: () => handleAllMails(row) },
@@ -196,6 +196,67 @@ onMounted(() => {
196
  fetchData();
197
  });
198
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
 
200
  </script>
201
 
@@ -203,6 +264,8 @@ onMounted(() => {
203
  <div class="w-full h-full flex flex-col p-2 md:p-5 gap-2 md:gap-5">
204
  <div class="flex flex-col md:flex-row md:items-center md:justify-between">
205
  <div class="flex flex-col md:flex-row md:items-center gap-4">
 
 
206
  <t-input v-model="userSearch" class="w-full md:w-40" placeholder="搜索用户" clearable />
207
  </div>
208
  </div>
@@ -213,6 +276,7 @@ onMounted(() => {
213
  <template #action="{ row }">
214
  <!-- 桌面端显示 -->
215
  <div class="hidden md:flex flex-wrap gap-2">
 
216
  <t-button size="small" variant="outline" @click="handleRefreshAuth(row)">刷新认证</t-button>
217
  <t-button size="small" variant="outline" @click="handleLatestMails(row)">最新邮件</t-button>
218
  <t-button size="small" variant="outline" @click="handleAllMails(row)">所有邮件</t-button>
 
28
  }
29
  ];
30
 
 
31
  // 添加下拉菜单选项函数
32
  const actionOptions = (row: Account) => [
33
+ { content: '查看认证', value: 'check', onClick: () => handleCheckAuth(row) },
34
  { content: '刷新认证', value: 'refresh', onClick: () => handleRefreshAuth(row) },
35
  { content: '最新邮件', value: 'latest', onClick: () => handleLatestMails(row) },
36
  { content: '所有邮件', value: 'all', onClick: () => handleAllMails(row) },
 
196
  fetchData();
197
  });
198
 
199
+ // Add new methods for batch refresh
200
+ const handleRefreshAll = async () => {
201
+ try {
202
+ loading.value = true;
203
+ const emails = allData.value.map(account => account.email);
204
+ const result = await mailApi.refreshAuthBatch(emails);
205
+
206
+ // Count successes and failures
207
+ const successes = result.filter((r:any) => r.success).length;
208
+ const failures = result.length - successes;
209
+
210
+ if (failures > 0) {
211
+ MessagePlugin.warning(`刷新完成: ${successes}个成功, ${failures}个失败`);
212
+ } else {
213
+ MessagePlugin.success('所有账号刷新成功');
214
+ }
215
+ } catch (error: any) {
216
+ MessagePlugin.error(`批量刷新失败: ${error.message}`);
217
+ } finally {
218
+ loading.value = false;
219
+ }
220
+ };
221
+
222
+ const handleRefreshPage = async () => {
223
+ try {
224
+ loading.value = true;
225
+ const emails = processedData.value.map(account => account.email);
226
+ const result = await mailApi.refreshAuthBatch(emails);
227
+
228
+ const successes = result.filter((r:any) => r.success).length;
229
+ const failures = result.length - successes;
230
+
231
+ if (failures > 0) {
232
+ MessagePlugin.warning(`刷新完成: ${successes}个成功, ${failures}个失败`);
233
+ } else {
234
+ MessagePlugin.success('当前页账号刷新成功');
235
+ }
236
+ } catch (error: any) {
237
+ MessagePlugin.error(`批量刷新失败: ${error.message}`);
238
+ } finally {
239
+ loading.value = false;
240
+ }
241
+ };
242
+
243
+ // 添加查看认证状态处理函数
244
+ const handleCheckAuth = async (row: Account) => {
245
+ try {
246
+ loading.value = true;
247
+ const result = await mailApi.getAuthStatus(row.email);
248
+ if (result.authenticated) {
249
+ MessagePlugin.success(`认证状态: ${result.message}\n认证时间: ${new Date(result.timestamp).toLocaleString()}`);
250
+ } else {
251
+ MessagePlugin.warning(`认证状态: ${result.message}`);
252
+ }
253
+ } catch (error: any) {
254
+ MessagePlugin.error(`检查认证失败: ${error.message}`);
255
+ } finally {
256
+ loading.value = false;
257
+ }
258
+ };
259
+
260
 
261
  </script>
262
 
 
264
  <div class="w-full h-full flex flex-col p-2 md:p-5 gap-2 md:gap-5">
265
  <div class="flex flex-col md:flex-row md:items-center md:justify-between">
266
  <div class="flex flex-col md:flex-row md:items-center gap-4">
267
+ <t-button @click="handleRefreshAll">刷新全部</t-button>
268
+ <t-button @click="handleRefreshPage">刷新本页</t-button>
269
  <t-input v-model="userSearch" class="w-full md:w-40" placeholder="搜索用户" clearable />
270
  </div>
271
  </div>
 
276
  <template #action="{ row }">
277
  <!-- 桌面端显示 -->
278
  <div class="hidden md:flex flex-wrap gap-2">
279
+ <t-button size="small" variant="outline" @click="handleCheckAuth(row)">查看认证</t-button>
280
  <t-button size="small" variant="outline" @click="handleRefreshAuth(row)">刷新认证</t-button>
281
  <t-button size="small" variant="outline" @click="handleLatestMails(row)">最新邮件</t-button>
282
  <t-button size="small" variant="outline" @click="handleAllMails(row)">所有邮件</t-button>