Skip to content

Commit 36161bd

Browse files
committed
feat(email): 支持邮箱别名规范化识别与显示优化
1 parent 2b082e8 commit 36161bd

File tree

9 files changed

+132
-43
lines changed

9 files changed

+132
-43
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

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

7-
**当前版本:V5.0** - 全新 UI 设计,深色模式支持,优化用户管理页面的布局
7+
**当前版本:V5.1** - 邮箱别名规范化支持,xx.abc@ex.com 邮件会收到 abc@ex.com
88

99
`转发的地址需要在cloudflare Email Addresses中验证`
1010

@@ -48,6 +48,7 @@
4848

4949
| 版本 | 主要更新 |
5050
|------|----------|
51+
| **V5.1** | 邮箱别名规范化支持 · xx.abc@ex.co 邮件会收到 abc@ex.co |
5152
| **V5.0** | 全新 UI · SVG 图标 · 深色模式 · 管理面板统计与布局优化 |
5253
| **V4.8** | 单个邮箱转发 · 收藏功能 · 按状态筛选 |
5354
| **V4.5** | 多域名 Resend 密钥配置 |

public/css/app-compose.css

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -250,18 +250,19 @@
250250
color: var(--muted);
251251
font-size: 13px;
252252
}
253-
.email-meta{ /* 统一左对齐布局:发件人标签/发件人内容/时间 */
254-
display: grid;
255-
grid-template-columns: 1fr auto; /* 左侧内容自适应,右侧时间固定自动宽度 */
256-
gap: 10px;
253+
.email-meta{ /* 统一布局:发件人/收件人/时间 */
254+
display: flex;
255+
flex-wrap: wrap;
257256
align-items: center;
257+
gap: 8px 12px;
258258
}
259259
.email-meta .meta-from{
260260
display: flex;
261261
align-items: center;
262262
gap: 6px;
263-
min-width: 0; /* 允许收缩 */
264-
overflow: hidden; /* 防止内容溢出 */
263+
min-width: 0;
264+
max-width: 280px;
265+
overflow: hidden;
265266
}
266267
.email-meta .meta-label{
267268
font-size: 12px;
@@ -271,23 +272,40 @@
271272
border-radius: 999px;
272273
padding: 2px 8px;
273274
white-space: nowrap;
274-
flex-shrink: 0; /* 标签不收缩 */
275+
flex-shrink: 0;
275276
}
276277
.email-meta .meta-from-text{
277278
color: var(--text);
278279
font-weight: 600;
279280
white-space: nowrap;
280281
overflow: hidden;
281282
text-overflow: ellipsis;
282-
min-width: 0; /* 允许收缩 */
283+
min-width: 0;
284+
}
285+
.email-meta .meta-to{
286+
display: flex;
287+
align-items: center;
288+
gap: 6px;
289+
min-width: 0;
290+
max-width: 280px;
291+
overflow: hidden;
292+
}
293+
.email-meta .meta-to-text{
294+
color: var(--primary);
295+
font-weight: 500;
296+
font-size: 13px;
297+
white-space: nowrap;
298+
overflow: hidden;
299+
text-overflow: ellipsis;
300+
min-width: 0;
283301
}
284302
.email-time{
285303
color: var(--muted2);
286304
font-size: 0.85em;
287305
font-weight: 500;
288-
justify-self: end; /* 时间右对齐 */
289-
text-align: right;
290-
white-space: nowrap; /* 防止时间换行 */
306+
margin-left: auto; /* 推到右侧 */
307+
white-space: nowrap;
308+
flex-shrink: 0;
291309
}
292310

293311
.email-actions{

public/css/app-mobile.css

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -99,18 +99,19 @@
9999

100100
/* 移动端邮件列表布局全面重构 */
101101
.email-meta{
102-
display: grid;
103-
grid-template-columns: 1fr 88px;
104-
gap: 8px;
105-
align-items: start;
102+
display: flex;
103+
flex-wrap: wrap;
104+
align-items: center;
105+
gap: 6px 10px;
106106
margin-bottom: 8px;
107107
}
108108

109109
.email-meta .meta-from{
110110
display: flex;
111111
align-items: center;
112-
gap: 6px;
112+
gap: 5px;
113113
min-width: 0;
114+
max-width: calc(100vw - 140px);
114115
}
115116

116117
.email-meta .meta-label{
@@ -132,18 +133,34 @@
132133
overflow: hidden;
133134
text-overflow: ellipsis;
134135
min-width: 0;
135-
max-width: calc(100vw - 148px);
136+
}
137+
138+
.email-meta .meta-to{
139+
display: flex;
140+
align-items: center;
141+
gap: 5px;
142+
min-width: 0;
143+
max-width: calc(100vw - 140px);
144+
}
145+
146+
.email-meta .meta-to-text{
147+
color: var(--primary);
148+
font-weight: 500;
149+
font-size: 12px;
150+
white-space: nowrap;
151+
overflow: hidden;
152+
text-overflow: ellipsis;
153+
min-width: 0;
136154
}
137155

138156
.email-time{
139157
display: flex;
140-
flex-direction: column;
141-
align-items: flex-end;
158+
align-items: center;
142159
font-size: 11px;
143160
color: var(--muted2);
144-
line-height: 1.1;
145-
text-align: right;
146-
gap: 1px;
161+
margin-left: auto;
162+
white-space: nowrap;
163+
flex-shrink: 0;
147164
}
148165

149166
.email-time .time-icon{

public/js/modules/app/email-list.js

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -150,28 +150,30 @@ export function renderEmailItem(email, isMobile = false) {
150150
const listCode = (e.verification_code || '').toString().trim() || extractCode(rawContent || '');
151151
const senderText = escapeHtml(e.sender || '');
152152

153+
// 解析收件人地址(用于发件箱和收件箱)
153154
let recipientsDisplay = '';
154-
if (isSentView) {
155-
const raw = (e.recipients || e.to_addrs || '').toString();
156-
const arr = raw.split(',').map(s => s.trim()).filter(Boolean);
157-
if (arr.length) {
158-
recipientsDisplay = arr.slice(0, 2).join(', ');
159-
if (arr.length > 2) recipientsDisplay += ` 等${arr.length}人`;
160-
} else {
161-
recipientsDisplay = raw;
162-
}
155+
const rawToAddrs = (e.recipients || e.to_addrs || '').toString();
156+
const toAddrsArr = rawToAddrs.split(',').map(s => s.trim()).filter(Boolean);
157+
if (toAddrsArr.length) {
158+
recipientsDisplay = toAddrsArr.slice(0, 2).join(', ');
159+
if (toAddrsArr.length > 2) recipientsDisplay += ` 等${toAddrsArr.length}人`;
160+
} else {
161+
recipientsDisplay = rawToAddrs;
163162
}
164163

165164
const subjectText = escapeHtml(e.subject || '(无主题)');
166165
const previewText = escapeHtml(preview);
167166
const metaLabel = isSentView ? '收件人' : '发件人';
168167
const metaText = isSentView ? escapeHtml(recipientsDisplay) : senderText;
169168
const timeDisplay = isMobile ? formatTsMobile(e.received_at || e.created_at) : formatTs(e.received_at || e.created_at);
169+
// 收件箱视图时显示收件人地址(别名地址)
170+
const toAddrDisplay = !isSentView && recipientsDisplay ? escapeHtml(recipientsDisplay) : '';
170171

171172
return `
172173
<div class="email-item clickable" onclick="${isSentView ? `showSentEmail(${e.id})` : `showEmail(${e.id})`}">
173174
<div class="email-meta">
174175
<span class="meta-from"><span class="meta-label">${metaLabel}</span><span class="meta-from-text">${metaText}</span></span>
176+
${!isSentView && toAddrDisplay ? `<span class="meta-to"><span class="meta-label">收件人</span><span class="meta-to-text">${toAddrDisplay}</span></span>` : ''}
175177
<span class="email-time"><span class="time-icon">🕐</span>${timeDisplay}</span>
176178
</div>
177179
<div class="email-content">

public/js/modules/app/email-viewer.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ export async function showEmailDetail(id, elements, api, showToast) {
2929
const code = email.verification_code || extractCode(email.content || email.html_content || '');
3030

3131
let metaHtml = `<div class="email-meta-inline">`;
32-
if (email.sender) metaHtml += `<span><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><use href="/icons/sprites.svg#icon-user"/></svg> ${escapeHtml(email.sender)}</span>`;
32+
if (email.sender) metaHtml += `<span><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><use href="/icons/sprites.svg#icon-user"/></svg> 发件人:${escapeHtml(email.sender)}</span>`;
33+
// 展示收件人地址(别名地址)
34+
if (email.to_addrs) metaHtml += `<span><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><use href="/icons/sprites.svg#icon-mail"/></svg> 收件人:${escapeHtml(email.to_addrs)}</span>`;
3335
if (email.received_at) {
3436
const d = new Date((email.received_at.includes('T') ? email.received_at : email.received_at.replace(' ', 'T')) + 'Z');
3537
const timeStr = new Intl.DateTimeFormat('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false, year: 'numeric', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' }).format(d);

src/api/emails.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export async function handleEmailsApi(request, db, url, path, options) {
4949

5050
try {
5151
const { results } = await db.prepare(`
52-
SELECT id, sender, subject, received_at, is_read, preview, verification_code
52+
SELECT id, sender, to_addrs, subject, received_at, is_read, preview, verification_code
5353
FROM messages
5454
WHERE mailbox_id = ?${timeFilter}
5555
ORDER BY received_at DESC
@@ -58,7 +58,7 @@ export async function handleEmailsApi(request, db, url, path, options) {
5858
return Response.json(results);
5959
} catch (e) {
6060
const { results } = await db.prepare(`
61-
SELECT id, sender, subject, received_at, is_read,
61+
SELECT id, sender, to_addrs, subject, received_at, is_read,
6262
CASE WHEN content IS NOT NULL AND content <> ''
6363
THEN SUBSTR(content, 1, 120)
6464
ELSE SUBSTR(COALESCE(html_content, ''), 1, 120)
@@ -110,7 +110,7 @@ export async function handleEmailsApi(request, db, url, path, options) {
110110
return Response.json(results || []);
111111
} catch (e) {
112112
const { results } = await db.prepare(`
113-
SELECT id, sender, subject, content, html_content, received_at, is_read
113+
SELECT id, sender, to_addrs, subject, content, html_content, received_at, is_read
114114
FROM messages WHERE id IN (${placeholders})${timeFilter}
115115
`).bind(...ids, ...timeParam).all();
116116
return Response.json(results || []);
@@ -236,7 +236,7 @@ export async function handleEmailsApi(request, db, url, path, options) {
236236
return Response.json({ ...row, content, html_content, download: row.r2_object_key ? `/api/email/${emailId}/download` : '' });
237237
} catch (e) {
238238
const { results } = await db.prepare(`
239-
SELECT id, sender, subject, content, html_content, received_at, is_read
239+
SELECT id, sender, to_addrs, subject, content, html_content, received_at, is_read
240240
FROM messages WHERE id = ?
241241
`).bind(emailId).all();
242242
if (!results || !results.length) return errorResponse('未找到邮件', 404);

src/email/receiver.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* @module email/receiver
44
*/
55

6-
import { extractEmail } from '../utils/common.js';
6+
import { extractEmail, normalizeEmailAlias } from '../utils/common.js';
77
import { getOrCreateMailboxId } from '../db/index.js';
88
import { parseEmailBody, extractVerificationCode } from './parser.js';
99

@@ -23,7 +23,9 @@ export async function handleEmailReceive(request, db, env) {
2323
const text = String(emailData?.text || '');
2424
const html = String(emailData?.html || '');
2525

26-
const mailbox = extractEmail(to);
26+
// 提取并规范化邮箱地址(支持别名邮箱,例如 ab.c@qq.ss -> c@qq.ss)
27+
const rawMailbox = extractEmail(to);
28+
const mailbox = normalizeEmailAlias(rawMailbox);
2729
const sender = extractEmail(from);
2830
const mailboxId = await getOrCreateMailboxId(db, mailbox);
2931

src/server.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import { initDatabase, getInitializedDatabase } from './db/index.js';
1414
import { createRouter, authMiddleware } from './routes/index.js';
1515
import { createAssetManager } from './assets/index.js';
16-
import { extractEmail } from './utils/common.js';
16+
import { extractEmail, normalizeEmailAlias } from './utils/common.js';
1717
import { forwardByLocalPart, forwardByMailboxConfig } from './email/forwarder.js';
1818
import { parseEmailBody, extractVerificationCode } from './email/parser.js';
1919
import { getForwardTarget } from './db/mailboxes.js';
@@ -94,10 +94,13 @@ export default {
9494

9595
const resolvedRecipient = (envelopeTo || toHeader || '').toString();
9696
const resolvedRecipientAddr = extractEmail(resolvedRecipient);
97-
const localPart = (resolvedRecipientAddr.split('@')[0] || '').toLowerCase();
97+
// 应用别名邮箱规范化:例如 ab.c@qq.ss -> c@qq.ss
98+
const normalizedRecipientAddr = normalizeEmailAlias(resolvedRecipientAddr);
99+
const localPart = (normalizedRecipientAddr.split('@')[0] || '').toLowerCase();
98100

99101
// 处理邮件转发(优先使用邮箱配置,否则使用全局规则)
100-
const mailboxForwardTo = await getForwardTarget(DB, resolvedRecipientAddr);
102+
// 使用规范化后的地址查询转发配置
103+
const mailboxForwardTo = await getForwardTarget(DB, normalizedRecipientAddr);
101104
if (mailboxForwardTo) {
102105
forwardByMailboxConfig(message, mailboxForwardTo, ctx);
103106
} else {
@@ -121,7 +124,8 @@ export default {
121124
htmlContent = '';
122125
}
123126

124-
const mailbox = extractEmail(resolvedRecipient || toHeader);
127+
// 使用规范化后的地址作为实际收件箱
128+
const mailbox = normalizedRecipientAddr || normalizeEmailAlias(extractEmail(toHeader));
125129
const sender = extractEmail(fromHeader);
126130

127131
// 存储到 R2

src/utils/common.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,49 @@ export function extractEmail(addr) {
1616
return s.split(/\s/)[0] || s;
1717
}
1818

19+
/**
20+
* 规范化邮箱别名地址
21+
* 类似谷歌别名邮箱功能:点号(.)前面的部分被视为别名前缀
22+
* 只保留最后一个点后面的部分作为真正的本地部分
23+
*
24+
* 例如:
25+
* - ab.c@qq.ss → c@qq.ss
26+
* - cds.c@qq.ss → c@qq.ss
27+
* - x.y.z@domain.com → z@domain.com
28+
* - simple@domain.com → simple@domain.com (无点则保持不变)
29+
*
30+
* @param {string} email - 邮箱地址
31+
* @returns {string} 规范化后的邮箱地址
32+
*/
33+
export function normalizeEmailAlias(email) {
34+
const normalized = String(email || '').trim().toLowerCase();
35+
if (!normalized) return '';
36+
37+
const atIndex = normalized.indexOf('@');
38+
if (atIndex <= 0) return normalized;
39+
40+
const localPart = normalized.slice(0, atIndex);
41+
const domain = normalized.slice(atIndex + 1);
42+
43+
// 查找本地部分中最后一个点的位置
44+
const lastDotIndex = localPart.lastIndexOf('.');
45+
46+
// 如果没有点,或者点在第一个位置,保持原样
47+
if (lastDotIndex <= 0) {
48+
return normalized;
49+
}
50+
51+
// 取最后一个点后面的部分作为真正的本地部分
52+
const realLocalPart = localPart.slice(lastDotIndex + 1);
53+
54+
// 如果点后面没有内容,保持原样
55+
if (!realLocalPart) {
56+
return normalized;
57+
}
58+
59+
return `${realLocalPart}@${domain}`;
60+
}
61+
1962
/**
2063
* 生成指定长度的随机ID
2164
* @param {number} length - ID长度,默认为8

0 commit comments

Comments
 (0)