一、 系统架构
零服务器成本:纯 Serverless 架构,完全基于 Cloudflare 免费套餐(Email Routing + Worker + D1 数据库),不需要租用任何 VPS。
多渠道自动分流:开启 Catch-all 规则,任意前缀(如 [email protected] 、[email protected] )随时可用,自动标记渠道。
毫秒级双重响应:
Telegram 手机秒弹:来信 1~2 秒内推送到手机,链接原生高亮可点。(目前测试响应挺快的 )
D1 终身归档备份:SQLite 数据库持久化存储,支持 SQL 检索。(CF只有5G的免费空间,有大容量附件的建议设置定时保存,我只是用来接验证码和工作日报完全够用 )
专业渲染 HTML 日报与账单:内置 Base64 / Quoted-Printable 深度解码引擎,Python 生成的复杂 HTML 报表、银行对账单在 Web 端原汁原味 1:1 渲染。
密码保护独立面板:绑定自定义二级域名(mail.yourdomain.com),带密码保护的响应式邮箱看板。
二、 前置准备条件
Cloudflare 账号
一个托管在 Cloudflare 上的自定义域名(本教程以 yourdomain.com 为例)。
TG机器人(需要用到bot token和个人id)
在 TG 找 @BotFather 发送 /newbot 获取 TG_BOT_TOKEN。
在 TG 找个可以获取自己的纯数字的机器人即可获取 TG_CHAT_ID。
务必在 TG 找到自己刚建的机器人,点击一次 Start 激活对话。
三、 全流程搭建步骤
1、开启 Cloudflare 邮件路由(Email Routing)
直接把你的域名添加到看板确定就可以了
2、创建 D1 数据库并初始化表结构
点击左侧主菜单 Storage & Databases(存储和数据库) -> D1 SQL Database。
点击 Create Database(创建数据库),名称写个自己喜欢的。
创建完成后,点击进入 mail_db,切换到 Console(控制台) 选项卡。(这里主要是创建收件的表结构,有特殊需求可以自行修改 )
粘贴并执行以下建表 SQL:
CREATE TABLE emails (
id INTEGER PRIMARY KEY AUTOINCREMENT,
message_from TEXT,
message_to TEXT,
subject TEXT,
body_text TEXT,
body_html TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
3、创建一体化 Worker(收信 + 看板 + TG 推送)
新建 Worker
访问左侧菜单 Compute -> Workers & Pages -> 点击 Create -> 选择 Worker。
这里我当时是选择了‘Hello World!’这个,如果有大佬开源的Worker用来收邮件也可以去github上找?(我没去找过QAQ)
命名为:mail-receiver(随便起名 ),点击右下角 Deploy(部署)。
贴入完整核心代码
点击右上方的Edit code,清空原有内容,将以下代码粘贴进去:(这里只是给个参考,可以让ai给你一个更适合你个人需求的代码 )
export default {
async email (message, env, ctx ) {
try {
const fromRaw = message.headers .get ('from' ) || message.from ;
const toRaw = message.to ;
const subjectRaw = message.headers .get ('subject' ) || '(无主题)' ;
const from = decodeHeader (fromRaw);
const subject = decodeHeader (subjectRaw);
const prefix = toRaw.split ('@' )[0 ].toUpperCase ();
let cleanText = '' ;
let cleanHtml = '' ;
try {
const rawText = await new Response (message.raw ).text ();
const parsed = parseMime (rawText);
cleanText = parsed.text ;
cleanHtml = parsed.html ;
} catch (e) {
cleanText = '(正文解析失败)' ;
}
try {
await env.DB .prepare (
`INSERT INTO emails (message_from, message_to, subject, body_text, body_html) VALUES (?, ?, ?, ?, ?)`
).bind (from , toRaw, subject, cleanText, cleanHtml).run ();
} catch (err) {
console .error ('写入数据库失败:' , err);
}
if (env.TG_BOT_TOKEN && env.TG_CHAT_ID ) {
const preview = cleanText.slice (0 , 800 ) + (cleanText.length > 800 ? '\n...(内容过长,请到网页端查看完整版)' : '' );
const tgText = `🔔 [${prefix} 渠道来信]\n\n` +
`👤 发件人: ${from } \n` +
`📌 主 题: ${subject} \n` +
`📥 收件箱: ${toRaw} \n\n` +
`📝 文本预览:\n${preview} ` ;
try {
await fetch (`https://api.telegram.org/bot${env.TG_BOT_TOKEN} /sendMessage` , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' },
body : JSON .stringify ({
chat_id : env.TG_CHAT_ID ,
text : tgText,
disable_web_page_preview : false
})
});
} catch (err) {}
}
} catch (globalErr) {
console .error ('邮件处理未知异常:' , globalErr);
}
return ;
},
async fetch (request, env, ctx ) {
const url = new URL (request.url );
const cookies = request.headers .get ('Cookie' ) || '' ;
const isAuthed = cookies.includes (`auth=${env.WEB_PASSWORD} ` );
if (url.pathname === '/login' && request.method === 'POST' ) {
const formData = await request.formData ();
if (formData.get ('pwd' ) === env.WEB_PASSWORD ) {
return new Response ('' , {
status : 302 ,
headers : {
'Location' : '/' ,
'Set-Cookie' : `auth=${env.WEB_PASSWORD} ; Path=/; HttpOnly; Max-Age=2592000; SameSite=Lax`
}
});
}
return new Response ('密码错误!<a href="/">返回</a>' , { headers : { 'Content-Type' : 'text/html;charset=utf-8' } });
}
if (!isAuthed && env.WEB_PASSWORD ) {
return new Response (renderLoginPage (), { headers : { 'Content-Type' : 'text/html;charset=utf-8' } });
}
if (url.pathname === '/render-html' ) {
const id = url.searchParams .get ('id' );
const row = await env.DB .prepare ('SELECT body_html, body_text FROM emails WHERE id = ?' ).bind (id).first ();
if (!row) return new Response ('未找到该邮件' );
return new Response (row.body_html || `<pre style="padding:20px;white-space:pre-wrap;">${row.body_text} </pre>` , {
headers : { 'Content-Type' : 'text/html;charset=utf-8' }
});
}
const list = await env.DB .prepare ('SELECT id, message_from, message_to, subject, created_at FROM emails ORDER BY id DESC LIMIT 50' ).all ();
const activeId = url.searchParams .get ('id' ) || (list.results [0 ] ? list.results [0 ].id : null );
let activeMail = null ;
if (activeId) {
activeMail = await env.DB .prepare ('SELECT * FROM emails WHERE id = ?' ).bind (activeId).first ();
}
return new Response (renderMailboxPage (list.results , activeMail), {
headers : { 'Content-Type' : 'text/html;charset=utf-8' }
});
}
};
function parseMime (raw ) {
let text = '' ;
let html = '' ;
const boundaryMatch = raw.match (/boundary="?([^";\r\n]+)"?/i );
if (boundaryMatch) {
const boundary = boundaryMatch[1 ];
const parts = raw.split ('--' + boundary);
for (const part of parts) {
if (/content-type:\s*text\/html/i .test (part)) {
html = extractBodyPart (part);
} else if (/content-type:\s*text\/plain/i .test (part)) {
text = extractBodyPart (part);
}
}
} else {
if (/content-type:\s*text\/html/i .test (raw)) {
html = extractBodyPart (raw);
} else {
text = extractBodyPart (raw);
}
}
return { text : text || '(纯 HTML 邮件)' , html : html || '' };
}
function extractBodyPart (part ) {
const chunks = part.split (/\r?\n\r?\n/ );
if (chunks.length < 2 ) return '' ;
const header = chunks[0 ];
let body = chunks.slice (1 ).join ('\n\n' ).replace (/--$/ , '' ).trim ();
if (/content-transfer-encoding:\s*base64/i .test (header)) {
try {
const bin = atob (body.replace (/\s/g , '' ));
const bytes = Uint8Array .from (bin, c => c.charCodeAt (0 ));
return new TextDecoder ('utf-8' ).decode (bytes);
} catch (e) { return body; }
}
if (/content-transfer-encoding:\s*quoted-printable/i .test (header) || body.includes ('=3D' ) || /=E[0-9A-F]/i .test (body)) {
try {
let s = body.replace (/=\s*[\r\n]+/g , '' );
s = s.replace (/%(?![0-9A-Fa-f]{2})/g , '%25' );
s = s.replace (/=([0-9A-Fa-f]{2})/g , '%$1' );
return decodeURIComponent (s);
} catch (e) {
return body.replace (/=([0-9A-Fa-f]{2})/g , (_, hex ) => String .fromCharCode (parseInt (hex, 16 )));
}
}
return body;
}
function decodeHeader (str ) {
if (!str) return '' ;
return str.replace (/=\?([^?]+)\?([BQbq])\?([^?]+)\?=/g , (_, charset, encoding, text ) => {
if (encoding.toUpperCase () === 'B' ) {
try {
const bin = atob (text.replace (/\s/g , '' ));
const bytes = Uint8Array .from (bin, c => c.charCodeAt (0 ));
return new TextDecoder (charset).decode (bytes);
} catch (e) { return text; }
}
return text;
});
}
function renderLoginPage ( ) {
return `<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>邮箱看板登录</title><style>body{background:#f3f4f6;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto;}form{background:#fff;padding:30px;border-radius:12px;box-shadow:0 4px 6px -1px rgba(0,0,0,.1);width:300px;}h2{margin:0 0 20px;text-align:center;font-size:20px;}input{width:100%;box-sizing:border-box;padding:10px;margin-bottom:15px;border:1px solid #d1d5db;border-radius:6px;}button{width:100%;padding:10px;background:#2563eb;color:#fff;border:none;border-radius:6px;font-weight:bold;cursor:pointer;}</style></head><body><form action="/login" method="POST"><h2>📬 私人邮箱看板</h2><input type="password" name="pwd" placeholder="请输入访问密码" required><button type="submit">解锁并进入</button></form></body></html>` ;
}
function renderMailboxPage (list, mail ) {
const mailItems = list.map (item => `
<a href="/?id=${item.id} " style="display:block;padding:14px;border-bottom:1px solid #e5e7eb;text-decoration:none;color:#1f2937;background:${mail && mail.id === item.id ? '#eff6ff' : '#fff' } ;">
<div style="font-weight:600;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${item.subject || '(无主题)' } </div>
<div style="font-size:12px;color:#6b7280;margin-top:4px;">${item.message_from} </div>
<div style="font-size:11px;color:#9ca3af;margin-top:2px;">渠道: <span style="background:#e5e7eb;padding:2px 4px;border-radius:4px;">${item.message_to.split('@' )[0 ]} </span> · ${item.created_at} </div>
</a>
` ).join ('' );
return `<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>云端邮箱控制台</title>
<style>
body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto;display:flex;height:100vh;overflow:hidden;background:#f9fafb;}
.sidebar{width:350px;border-right:1px solid #e5e7eb;display:flex;flex-direction:column;background:#fff;}
.sidebar-header{padding:16px;border-bottom:1px solid #e5e7eb;font-weight:bold;font-size:16px;display:flex;justify-content:space-between;align-items:center;}
.mail-list{flex:1;overflow-y:auto;}
.main-view{flex:1;display:flex;flex-direction:column;overflow:hidden;}
.mail-meta{padding:20px;border-bottom:1px solid #e5e7eb;background:#fff;}
.mail-body-container{flex:1;position:relative;background:#fff;}
iframe{width:100%;height:100%;border:none;}
@media(max-width:768px){
body{flex-direction:column;}
.sidebar{width:100%;height:${mail ? '200px' : '100vh' } ;}
.main-view{height:calc(100vh - 200px);}
}
</style>
</head>
<body>
<div class="sidebar">
<div class="sidebar-header">
<span>📬 邮件归档 (${list.length} )</span>
<a href="/" style="font-size:12px;text-decoration:none;color:#2563eb;">刷新</a>
</div>
<div class="mail-list">${mailItems || '<div style="padding:20px;color:#9ca3af;">暂无邮件</div>' } </div>
</div>
<div class="main-view">
${mail ? `
<div class="mail-meta">
<h2 style="margin:0 0 10px;font-size:18px;">${mail.subject} </h2>
<div style="font-size:13px;color:#4b5563;">发件人: <b>${mail.message_from} </b> · 收件箱: <code>${mail.message_to} </code></div>
<div style="font-size:12px;color:#9ca3af;margin-top:4px;">时间: ${mail.created_at} </div>
</div>
<div class="mail-body-container">
<iframe src="/render-html?id=${mail.id} " sandbox="allow-same-origin allow-popups"></iframe>
</div>
` : '<div style="padding:40px;text-align:center;color:#9ca3af;">请选择左侧邮件查看</div>' }
</div>
</body></html>` ;
}
粘贴后点击右上角 Deploy(部署)。
绑定 D1 数据库与环境变量
进入该 Worker 的 Bindings(绑定) -> D1 Database Bindings -> Add binding。
选择的D1 database,添加到Bindings
Variable name 填:DB(这个和上面的代码有关系,你代码怎么写的,这里就怎么填,依旧不懂问ai嘻嘻 )。
数据库选中:mail_db,保存并部署。
绑定环境变量(安全密码 & TG):
进入 Variables and Secrets(变量和机密) -> 点击 Add 添加三条:
WEB_PASSWORD:你自定义的 Web 看板访问密码(如 AdminPass123)。
TG_BOT_TOKEN:你的 Telegram 机器人 Token。
TG_CHAT_ID:你的 Telegram 个人数字 ID。
点击保存并部署。
4、配置邮件路由规则(Catch-all 全接收)
找到路由规则:Email(电子邮件) -> Email Routing(邮件路由) -> Routing rules(路由规则)。
找到 Catch-all(兜底规则),点击编辑:
操作(Action):选择 Send to a Worker(发送到 Worker)。
目标 Worker:选中 mail-receiver。
保存并确保该规则右侧的状态开关是已启用(Enabled)。
5、为 Web 看板绑定自定义域名
回到 Worker mail-receiver 的 Domains页面。
点击 Add -> 选择 Custom Domain(自定义域)。
输入:mail.yourdomain.com,点击确定。
四、 核心功能使用指南
外部邮箱多渠道分流转发
现在任何 @yourdomain.com 结尾的邮箱都能收信,并自动在面板和 TG 标记渠道。
网页端查看 Python HTML 自动化日报
打开浏览器访问:https://mail.yourdomain.com。
输入 WEB_PASSWORD 登录。
左侧点击邮件,右侧基于独立沙箱视窗渲染,Python 生成的表格、图表、颜色样式 1:1 呈现。
结语
本贴仅为本人纪念留贴,目的在于探索邮件转发集成的方法和CF的玩法,并非严格的教程贴,如果有什么细节没写到位可以自行和ai对话询问。
目前仅能实现邮件转发集成+TG机器人提醒+看板查看,无法回信和管理邮箱,仅作为多邮箱收件情况下的一种轻量部署解决方案。
^-^