Skip to content

Commit 27c1e6d

Browse files
fix 修复邮箱信息加载错误
1 parent fd47275 commit 27c1e6d

8 files changed

Lines changed: 101 additions & 36 deletions

File tree

README.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
一个基于 Cloudflare Workers + D1 + R2 构建的**开源临时邮箱服务**,支持邮件接收、发送、转发、用户管理等完整功能。
66

7-
**当前版本:V4.8** - 新增邮件转发和收藏功能
7+
**当前版本:V4.8** - 新增单个邮件转发和收藏功能
8+
9+
`转发的地址需要在cloudflare Email Addresses中验证`
810

911
📖 **[一键部署指南](docs/yijianbushu.md)** | 📬 **[Resend 发件配置](docs/resend.md)** | 📚 **[API 文档](docs/api.md)**
1012

@@ -133,7 +135,15 @@ RESEND_API_KEY='{"domain1.com":"re_key1","domain2.com":"re_key2"}'
133135
<details>
134136
<summary><strong>FORWARD_RULES 配置格式</strong></summary>
135137

136-
规则按前缀匹配,`*` 为兜底规则。转发目标需在 Cloudflare Email Addresses 中验证。
138+
规则按前缀匹配,`*` 为兜底规则。
139+
140+
⚠️ **重要**:转发目标邮箱必须在 Cloudflare 控制台中验证后才能使用:
141+
1. 进入 Cloudflare 控制台 → 域名 → 电子邮件 → 电子邮件路由
142+
2. 切换到「目标地址」选项卡
143+
3. 点击「添加目标地址」,输入转发目标邮箱
144+
4. 前往目标邮箱收取验证邮件并点击确认链接
145+
146+
![转发目标地址验证](pic/resend/zhuanfa.png)
137147

138148
```bash
139149
# 键值对格式

package-lock.json

Lines changed: 0 additions & 6 deletions
This file was deleted.

pic/resend/zhuanfa.png

51.6 KB
Loading

public/js/auth-guard.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ try{
2323
const controller = new AbortController();
2424
const timeout = setTimeout(()=>controller.abort(), 8000);
2525
const opts = { method: 'GET', headers: { 'Cache-Control': 'no-cache' }, keepalive: true, signal: controller.signal };
26-
const mailboxes = fetch('/api/mailboxes?limit=10&offset=0', opts).then(r => r.ok ? r.json() : []).then(data => save('mf:prefetch:mailboxes', Array.isArray(data) ? data : [] )).catch(()=>{});
26+
const mailboxes = fetch('/api/mailboxes?limit=10&offset=0', opts).then(r => r.ok ? r.json() : { list: [] }).then(data => save('mf:prefetch:mailboxes', Array.isArray(data) ? data : (data.list || []) )).catch(()=>{});
2727
const quota = fetch('/api/user/quota', opts).then(r => r.ok ? r.json() : null).then(data => { if (data) save('mf:prefetch:quota', data); }).catch(()=>{});
2828
const domains = fetch('/api/domains', opts).then(r => r.ok ? r.json() : []).then(list => { if (Array.isArray(list) && list.length) save('mf:prefetch:domains', list); }).catch(()=>{});
2929
// 不阻塞太久:最多等待 800ms 即跳转,其余继续后台完成(keepalive)

public/js/mailboxes.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ async function load() {
8787

8888
const data = await fetchMailboxes(params);
8989
const list = Array.isArray(data) ? data : (data.list || []);
90-
lastCount = list.length;
90+
const total = data.total ?? list.length;
91+
lastCount = total;
9192
currentData = list;
9293

9394
if (!list.length) {
@@ -110,9 +111,10 @@ async function load() {
110111

111112
// 更新分页器
112113
function updatePager() {
113-
if (els.page) els.page.textContent = `第 ${page} 页`;
114+
const totalPages = Math.max(1, Math.ceil(lastCount / PAGE_SIZE));
115+
if (els.page) els.page.textContent = `第 ${page} / ${totalPages} 页 (共 ${lastCount} 个)`;
114116
if (els.prev) els.prev.disabled = page <= 1;
115-
if (els.next) els.next.disabled = lastCount < PAGE_SIZE;
117+
if (els.next) els.next.disabled = page >= totalPages;
116118
}
117119

118120
// 绑定卡片事件
@@ -428,7 +430,10 @@ els.search?.addEventListener('click', () => { page = 1; load(); });
428430
els.q?.addEventListener('input', () => { if (searchTimeout) clearTimeout(searchTimeout); searchTimeout = setTimeout(() => { page = 1; load(); }, 300); });
429431
els.q?.addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); page = 1; load(); }});
430432
els.prev?.addEventListener('click', () => { if (page > 1 && !isLoading) { page--; load(); }});
431-
els.next?.addEventListener('click', () => { if (lastCount === PAGE_SIZE && !isLoading) { page++; load(); }});
433+
els.next?.addEventListener('click', () => {
434+
const totalPages = Math.max(1, Math.ceil(lastCount / PAGE_SIZE));
435+
if (page < totalPages && !isLoading) { page++; load(); }
436+
});
432437
els.domainFilter?.addEventListener('change', () => { page = 1; load(); });
433438
els.loginFilter?.addEventListener('change', () => { page = 1; load(); });
434439
els.favoriteFilter?.addEventListener('change', () => { page = 1; load(); });

public/js/modules/app/mock-api.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,10 +244,11 @@ export async function mockApi(path, options = {}) {
244244
// 分页
245245
const page = Number(url.searchParams.get('page') || 1);
246246
const size = Number(url.searchParams.get('size') || 20);
247+
const total = result.length;
247248
const start = (page - 1) * size;
248249
const pageResult = result.slice(start, start + size);
249250

250-
return new Response(JSON.stringify(pageResult), { headers: jsonHeaders });
251+
return new Response(JSON.stringify({ list: pageResult, total }), { headers: jsonHeaders });
251252
}
252253

253254
// POST /api/mailboxes/pin

src/api/mailboxes.js

Lines changed: 76 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -175,10 +175,16 @@ export async function handleMailboxesApi(request, db, mailDomains, url, path, op
175175
// 获取用户的邮箱列表
176176
if (path === '/api/mailboxes' && request.method === 'GET') {
177177
if (isMock) {
178+
const searchParam = url.searchParams.get('q');
178179
const domainParam = url.searchParams.get('domain');
179180
const favoriteParam = url.searchParams.get('favorite');
180181
const forwardParam = url.searchParams.get('forward');
181182
let results = buildMockMailboxes(MOCK_DOMAINS);
183+
// 搜索过滤
184+
if (searchParam && searchParam.trim()) {
185+
const q = searchParam.trim().toLowerCase();
186+
results = results.filter(m => m.address.toLowerCase().includes(q));
187+
}
182188
if (domainParam) {
183189
results = results.filter(m => m.address.endsWith('@' + domainParam));
184190
}
@@ -192,7 +198,15 @@ export async function handleMailboxesApi(request, db, mailDomains, url, path, op
192198
} else if (forwardParam === 'false' || forwardParam === '0') {
193199
results = results.filter(m => !m.forward_to);
194200
}
195-
return Response.json(results);
201+
// 分页
202+
const pageParam = url.searchParams.get('page');
203+
const sizeParam = url.searchParams.get('size');
204+
const page = Math.max(1, Number(pageParam || 1));
205+
const size = Math.max(1, Math.min(500, Number(sizeParam || 20)));
206+
const total = results.length;
207+
const start = (page - 1) * size;
208+
const pageResult = results.slice(start, start + size);
209+
return Response.json({ list: pageResult, total });
196210
}
197211

198212
const payload = getJwtPayload(request, options);
@@ -209,9 +223,9 @@ export async function handleMailboxesApi(request, db, mailDomains, url, path, op
209223
WHERE address = ?
210224
LIMIT 1
211225
`).bind(payload.mailboxAddress).all();
212-
return Response.json(results || []);
226+
return Response.json({ list: results || [], total: results?.length || 0 });
213227
} catch (e) {
214-
return Response.json([]);
228+
return Response.json({ list: [], total: 0 });
215229
}
216230
}
217231

@@ -232,10 +246,24 @@ export async function handleMailboxesApi(request, db, mailDomains, url, path, op
232246
}
233247
}
234248

235-
if (!uid && !strictAdmin) return Response.json([]);
249+
if (!uid && !strictAdmin) return Response.json({ list: [], total: 0 });
236250

237-
const limit = Math.max(1, Math.min(500, Number(url.searchParams.get('limit') || 100)));
238-
const offset = Math.max(0, Number(url.searchParams.get('offset') || 0));
251+
// 支持两种分页参数:page/size 或 limit/offset
252+
let limit, offset;
253+
const pageParam = url.searchParams.get('page');
254+
const sizeParam = url.searchParams.get('size');
255+
256+
if (pageParam !== null || sizeParam !== null) {
257+
// 使用 page/size 分页
258+
const page = Math.max(1, Number(pageParam || 1));
259+
const size = Math.max(1, Math.min(500, Number(sizeParam || 20)));
260+
limit = size;
261+
offset = (page - 1) * size;
262+
} else {
263+
// 使用 limit/offset 分页
264+
limit = Math.max(1, Math.min(500, Number(url.searchParams.get('limit') || 100)));
265+
offset = Math.max(0, Number(url.searchParams.get('offset') || 0));
266+
}
239267

240268
const bindParams = [];
241269
const whereConditions = [];
@@ -247,11 +275,18 @@ export async function handleMailboxesApi(request, db, mailDomains, url, path, op
247275
bindParams.push(uid);
248276
}
249277

278+
const searchParam = url.searchParams.get('q');
250279
const domainParam = url.searchParams.get('domain');
251280
const loginParam = url.searchParams.get('login');
252281
const favoriteParam = url.searchParams.get('favorite');
253282
const forwardParam = url.searchParams.get('forward');
254283

284+
// 搜索过滤(模糊匹配邮箱地址)
285+
if (searchParam && searchParam.trim()) {
286+
whereConditions.push('m.address LIKE ?');
287+
bindParams.push(`%${searchParam.trim().toLowerCase()}%`);
288+
}
289+
255290
if (domainParam) {
256291
whereConditions.push('m.domain = ?');
257292
bindParams.push(domainParam);
@@ -281,11 +316,25 @@ export async function handleMailboxesApi(request, db, mailDomains, url, path, op
281316
const whereClause = whereConditions.length > 0 ? 'WHERE ' + whereConditions.join(' AND ') : '';
282317
bindParams.push(limit, offset);
283318

319+
// 构建计数查询的参数(不包含 limit 和 offset)
320+
const countBindParams = bindParams.slice(0, -2);
321+
284322
// 严格管理员使用 LEFT JOIN 显示所有邮箱,同时保留自己的置顶状态
285323
// 普通用户使用 INNER JOIN 只显示自己关联的邮箱
286324
if (strictAdmin && uid) {
287325
// 严格管理员:显示所有邮箱,使用 LEFT JOIN 获取自己的置顶状态
288326
const adminBindParams = [uid, ...bindParams];
327+
const adminCountBindParams = [uid, ...countBindParams];
328+
329+
// 获取总数
330+
const countResult = await db.prepare(`
331+
SELECT COUNT(*) as total
332+
FROM mailboxes m
333+
LEFT JOIN user_mailboxes um ON m.id = um.mailbox_id AND um.user_id = ?
334+
${whereClause}
335+
`).bind(...adminCountBindParams).first();
336+
const total = countResult?.total || 0;
337+
289338
const { results } = await db.prepare(`
290339
SELECT m.id, m.address, m.created_at, COALESCE(um.is_pinned, 0) AS is_pinned,
291340
CASE WHEN (m.password_hash IS NULL OR m.password_hash = '') THEN 1 ELSE 0 END AS password_is_default,
@@ -297,9 +346,17 @@ export async function handleMailboxesApi(request, db, mailDomains, url, path, op
297346
ORDER BY COALESCE(um.is_pinned, 0) DESC, m.created_at DESC
298347
LIMIT ? OFFSET ?
299348
`).bind(...adminBindParams).all();
300-
return Response.json(results || []);
349+
return Response.json({ list: results || [], total });
301350
} else if (strictAdmin) {
302351
// 严格管理员但没有 uid(不应该发生,但作为兜底)
352+
// 获取总数
353+
const countResult = await db.prepare(`
354+
SELECT COUNT(*) as total
355+
FROM mailboxes m
356+
${whereClause}
357+
`).bind(...countBindParams).first();
358+
const total = countResult?.total || 0;
359+
303360
const { results } = await db.prepare(`
304361
SELECT m.id, m.address, m.created_at, 0 AS is_pinned,
305362
CASE WHEN (m.password_hash IS NULL OR m.password_hash = '') THEN 1 ELSE 0 END AS password_is_default,
@@ -310,9 +367,18 @@ export async function handleMailboxesApi(request, db, mailDomains, url, path, op
310367
ORDER BY m.created_at DESC
311368
LIMIT ? OFFSET ?
312369
`).bind(...bindParams).all();
313-
return Response.json(results || []);
370+
return Response.json({ list: results || [], total });
314371
} else {
315372
// 普通用户:只显示自己关联的邮箱
373+
// 获取总数
374+
const countResult = await db.prepare(`
375+
SELECT COUNT(*) as total
376+
FROM user_mailboxes um
377+
JOIN mailboxes m ON m.id = um.mailbox_id
378+
${whereClause}
379+
`).bind(...countBindParams).first();
380+
const total = countResult?.total || 0;
381+
316382
const { results } = await db.prepare(`
317383
SELECT m.id, m.address, m.created_at, um.is_pinned,
318384
CASE WHEN (m.password_hash IS NULL OR m.password_hash = '') THEN 1 ELSE 0 END AS password_is_default,
@@ -324,10 +390,10 @@ export async function handleMailboxesApi(request, db, mailDomains, url, path, op
324390
ORDER BY um.is_pinned DESC, m.created_at DESC
325391
LIMIT ? OFFSET ?
326392
`).bind(...bindParams).all();
327-
return Response.json(results || []);
393+
return Response.json({ list: results || [], total });
328394
}
329395
} catch (_) {
330-
return Response.json([]);
396+
return Response.json({ list: [], total: 0 });
331397
}
332398
}
333399

wrangler.toml

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,10 @@ compatibility_date = "2024-01-01"
88
binding = "TEMP_MAIL_DB"
99
database_name = "maill_free_db" #你的d1数据库名称
1010
database_id = "c82663ac-62b6-4ba6-be81-cbe297f2a3dd" # 你的database_id
11-
11+
# 登录会话过期时间(单位:天,默认7天)
1212
# 环境变量
1313
[vars]
14-
ADMIN_NAME="iding"
15-
ADMIN_PASSWORD="asdfgh"
16-
GUEST_PASSWORD="adminasdawawdwa"
17-
JWT_TOKEN="fjiwaiv23164s4f6df6wfdx"
18-
JWT_TOKENGUEST_PASSWORD="fzsjfizw874984esfu8df7sr3"
19-
# 登录会话过期时间(单位:天,默认7天)
20-
SESSION_EXPIRE_DAYS="7"
2114
# FORWARD_RULES="[{"prefix":"vip","email":"a@example.com"},{"prefix":"*","email":"2141083706@qq.com"}]"
22-
# MAIL_DOMAIN="idinging.com,iding.me,dding.asia,iding.asia,dinging.top,iding.qzz.io,12138.qzz.io,iding.cloudns.ch,iding.x10.mx,iding.elementfx.com,iding.x10host.com,izhou.x10.mx,952405.dpdns.org,"
23-
MAIL_DOMAIN="example.com,abc.ef"
24-
RESEND_API_KEY="dinging.top=re_ahF44aPK_2SERJuTLxcN8t86ChXXYpcBY,iding.qzz.io=re_J2fKFbBW_2xYbv8cnDLpqnRtcx5FDHBuh,iding.asia=re_3KH4SQ6M_KkSSbvAvx9QCYCMGGXzqUWHK"
25-
2615

2716

2817
# 静态资源目录(Workers + Assets)

0 commit comments

Comments
 (0)