Source & license
Upstream license: GPL-3.0
License TL;DR
You can run it and change it privately. If you give others copies of the program or a modified version, provide the corresponding source under the GPL. You can charge money. Running it as a web service alone doesn’t trigger that source-sharing requirement.
Explain GPL v3 in plain English →Summary of the main license. Separate packages and assets can have different terms.
Inspect repository ↗Read this project’s actual license ↗Repository owner
See the upstream repository for the original creator and contributors.
Maintain this project? Maintainer verification →Cloudflare hosting
Free tier eligible within limits
The reviewed Cloudflare deployment is eligible for Free-plan allowances for the stated small workload and feature scope. Usage limits, CPU, required account setup and separate services apply.
Hosting requirements
- Free hosting scope is receiving only with SEND_CHANNEL unset; outbound email is hidden until separately configured.
- Provide an owned domain on Cloudflare and catch-all Email Routing; registration/domain cost is separate from Worker hosting.
- Replace all template placeholders and cookie/access secrets, keep Worker CPU/requests and D1 rows/storage below Free limits.
- API access is off by default until ENABLE_OPENAPI is true; any sending requires provider account restrictions, verified sender and its own pricing review.
- The active email-sending binding is observed but its presence does not prove free delivery to arbitrary recipients.
- Workers Free dynamic requests are shared across this account (100,000/day), with 10 ms CPU per invocation; workload fit is conditional and has not been measured.
- D1 Free allowance: 5 million rows read/day, 100,000 rows written/day and 5 GB total storage; unindexed scans and history retention consume quota.
Sources checked 01/10/2026
Repository snapshot: e78cb52. Hosting eligibility reflects the deployment documentation and listed assumptions.
- temp-mail ↗
ub.com/oiov/vmail/blob/main/README_en.md">English</a> | 简体中文</p> <p>使用 Cloudflare Email Worker 实现的临时电子邮件服务</p> </div> > 🌟 推荐 **Claude Code** 稳定 API 渠道:[Nbility AI Gateway](https://nbility.ai/auth/register?aff=Dptp) ,支持 Claude Fable 5、GPT 5.6 等主流 AI Coding 大模型 ## 🌈 特点 - 🎯 隐私友好,无需注册,开箱即用 - ✈️ 支持邮件收发 - ✨ 支持保存密码,找回邮箱 - 😄 支持多域名后缀 - 🔌 **开放 RESTful API**,支持程序化调用 - 🚀 快速部署,纯 Cloudflare 方案,无需服务器 原理: - Email worker 接收电子邮件 - 前端 (Vite + React) 显示电子邮件 - 邮件存储 (Cloudflare D1) - 发信使用 Resend API、MailChannels API 或 Cloudflare 原生发件 ## 👋 自部署教程 本项目基于 Cloudflare Workers
- mailinator ↗
` 该命令会同时启动前端 Vite 开发服务器和本地的 Wrangler Worker 环境。 ## 📖 API 文档 Vmail 提供完整的 RESTful API,支持通过程序化方式创建临时邮箱、查询收件箱。 ### 获取 API Key 访问 [API 文档页面](https://vmail.dev/api-docs) 创建免费的 API Key。 ### API 端点 | 方法 | 端点 | 说明 | | -------- | ------------------------------------------- | ---------------------- | | `POST` | `/api/v1/mailboxes` | 创建临时邮箱 | | `GET` | `/api/v1/mailboxes/:id` | 获取邮箱信息 | | `GET` | `/api/v1/mailboxes/:id/messages`
- workers ↗
name = "vmail" main = "worker/src/index.ts" # 打包配置 minify = true compatibility_flags = [ "nodejs_compat" ] compatibility_date = "2025-03-27" keep_vars = true # 启用 D1 数据库 [[d1_databases]] binding = "DB" database_name = "${D1_DATABASE_NAME}" database_id = "${D1_DATABASE_ID}" # fix: 将迁移目录直接配置在这里,这是 D1 自动迁移的标准做法 migrations_dir = "worker/drizzle" # 定义环境变量和密钥 [vars] PASSWORD = "${PASSWORD}" COOKIES_SECRET = "${COOKIES_SECRET}" EMAIL_DOMAIN = "${EMAIL_DOMAIN}" SEND_CHANNEL = "${SEND_C
- d1 ↗
mpat" ] compatibility_date = "2025-03-27" keep_vars = true # 启用 D1 数据库 [[d1_databases]] binding = "DB" database_name = "${D1_DATABASE_NAME}" database_id = "${D1_DATABASE_ID}" # fix: 将迁移目录直接配置在这里,这是 D1 自动迁移的标准做法 migrations_dir = "worker/drizzle" # 定义环境变量和密钥 [vars] PASSWORD = "${PASSWORD}" COOKIES_SECRET = "${COOKIES_SECRET}" EMAIL_DOMAIN = "${EMAIL_DOMAIN}" SEND_CHANNEL = "${SEND_CHANNEL}" SENDER_EMAIL = "${SENDER_EMAIL}" SEND_RATE_LIMIT_PER_MINUTE = "${SEND_RATE_LIMIT_PER_MINUTE}" SEND_IP_RATE_LIMIT_PER_MINUTE = "${SEND_IP_RATE_LIMIT_PER_MINUTE}" TURNSTILE_KEY =
- email-workers ↗
✨ 支持保存密码,找回邮箱 - 😄 支持多域名后缀 - 🔌 **开放 RESTful API**,支持程序化调用 - 🚀 快速部署,纯 Cloudflare 方案,无需服务器 原理: - Email worker 接收电子邮件 - 前端 (Vite + React) 显示电子邮件 - 邮件存储 (Cloudflare D1) - 发信使用 Resend API、MailChannels API 或 Cloudflare 原生发件 ## 👋 自部署教程 本项目基于 Cloudflare Workers 和 Cloudflare D1 构建。您只需要一个托管在 Cloudflare 上的域名即可。 ### 准备工作 - [Cloudflare](https://dash.cloudflare.com/) 账户与托管在 Cloudflare 上的域名 - 本地安装 [Node.js](https://nodejs.org) 环境(版本 >= 22.x)和 [pnpm](https://pnpm.io/installation) ### 自动部署 (推荐) 本项目已包含一个预先配置好的 GitHub Action 工作流,可以帮助您自动将 Vmail 应用部署到 Cloudflare。 详细步骤请参考
- free-tier-eligible ↗
name = "vmail" main = "worker/src/index.ts" # 打包配置 minify = true compatibility_flags = [ "nodejs_compat" ] compatibility_date = "2025-03-27" keep_vars = true # 启用 D1 数据库 [[d1_databases]] binding = "DB" database_name = "${D1_DATABASE_NAME}" database_id = "${D1_DATABASE_ID}" # fix: 将迁移目录直接配置在这里,这是 D1 自动迁移的标准做法 migrations_dir = "worker/drizzle" # 定义环境变量和密钥 [vars] PASSWORD = "${PASSWORD}" COOKIES_SECRET = "${COOKIES_SECRET}" EMAIL_DOMAIN = "${EMAIL_DOMAIN}" SEND_CHAN
- free-tier-eligible ↗
ount Manager. | | Requests<sup>1, 2, 3, 4</sup> | Duration | CPU time | | --- | --- | --- | --- | | **Free** | 100,000 per day | No charge for duration | 10 milliseconds of CPU time per invocation | | **Standard** | 10 million included per month <br> +$0.30 per additional million | No charge or limit for duration | 30 million CPU milliseconds included per month<br> +$0.02 per additional million CPU milliseconds<br><br> Max of [5 minutes of CPU time](https://developers.cloudflare.com/workers/platform/limits/#account-plan-limits) per invocation (default: 30 second
- free-tier-eligible ↗
rs Paid](https://developers.cloudflare.com/workers/platform/pricing/#workers) | | --- | --- | --- | | Rows read | 5 million / day | First 25 billion / month included + $0.001 / million rows | | Rows written | 100,000 / day | First 50 million / month included + $1.00 / million rows | | Storage (per GB stored) | 5 GB (total) | First 5 GB included + $0.75 / GB-mo | Track your D1 usage To accurately track your usage, use the [meta object](https://developers.cloudflare.com/d1/worker-api/return-object/), [GraphQL Analytics API](https://developers.cloudflare.com/d1/obs
- free-tier-eligible ↗
loudflare Email Service pricing is based on your Cloudflare plan and email usage. ## Plan pricing Email Routing is available on both the Workers Free and Workers Paid plans. Sending to arbitrary recipients requires the Workers Paid plan. Sending to [verified destination addresses](https://developers.cloudflare.com/email-service/configuration/email-routing-addresses/#destination-addresses) in your account is free on all plans, including when only Email Routing is configured. | | Workers Free | Workers Paid | | --- | --- | --- | | **Outbound emails (Email Sendin
- GPL-3.0 ↗
GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and cha
- architecture ↗
name = "vmail" main = "worker/src/index.ts" # 打包配置 minify = true compatibility_flags = [ "nodejs_compat" ] compatibility_date = "2025-03-27" keep_vars = true # 启用 D1 数据库 [[d1_databases]] binding = "DB" database_name = "${D1_DATABASE_NAME}" database_id = "${D1_DATABASE_ID}" # fix: 将迁移目录直接配置在这里,这是 D1 自动迁移的标准做法 migrations_dir = "worker/drizzle" # 定义环境变量和密钥 [vars] PASSWORD = "${PASSWORD}" COOKIES_SECRET = "${COOKIES_SECRET}" EMAIL_DOMAIN = "${EMAIL_DOMAIN}" SEND_CHANNEL = "${SEND_C
Upstream screenshot · oiov/vmail repository contributors ↗. Depicts the upstream project. We have not deployed and tested a fresh installation here.
What it can replace
Compare the workflow you need. These mappings describe overlap; full feature parity requires a separate comparison.
Editorial workflow alternative: Disposable inbox creation and message reading on an owned domain; no anonymity or provider-deliverability equivalence.
See supporting source ↗Editorial workflow alternative: Temporary mailbox receiving and optional programmatic inbox APIs; no enterprise testing, public-inbox or throughput parity claim.
See supporting source ↗How it works
The shape of Vmail on Cloudflare, and how it stacks up against the rented tools it replaces.
Architecture
Diagram of deployment declarations at the reviewed commit. Each app has its own entrypoint; declared resources do not prove runtime calls. Follow file and line sources below.
View upstream source ↗Configuration and workflow sources
Reviewed commit e78cb52ad450. Files were read as data; upstream applications and CI jobs were not executed.
Deployment configuration · 1 files
Cloudflare Workers · compatibility 2025-03-27
vmail · default
Entrypoint: worker/src/index.ts
Build: pnpm run build
Static assets: frontend/build/client
Cron triggers (UTC): 0 * * * *
DB→ D1SEND_EMAIL→ Send EmailASSETS→ Static assets
Named environments are separate deployments. Bindings are shown only where declared. Configured routes are URL patterns, not verified application endpoints.
Runtime source · handlers, binding usage and workflow steps
Observed TypeScript/JavaScript declarations from Worker entrypoints and resolved relative imports. Calls and workflow steps may run conditionally; their listed order is not a proven end-to-end request flow. Router declarations may be mounted under a prefix or may not be registered. This shows code wiring, not a successful deployment or runtime test. Dynamic wiring, aliases and generated code may not resolve.
- L598 · email handler exported · references DB · calls getD1DB, text, parse, nanoid, insertEmailSchema.parse, insertEmail, incrementEmailsReceived, incrementDailyEmailsReceived, console.error, message.setReject
- L650 · fetch handler exported · references ASSETS · calls shouldBypassSiteGate, isSiteUnlocked, JSON.stringify, url.pathname.startsWith, app.fetch, env.ASSETS.fetch, toString
- L681 · scheduled handler exported · references DB · calls getD1DB, Date.now, deleteExpiredEmails, console.log, oneDayAgo.toISOString
- L57 · app.use("/api/v1/*")
- L494 · app.get("/config")
- L546 · app.post("/auth/unlock")
- L570 · app.get("/auth/status")
- L578 · app.post("/auth/logout")
- L587 · app.route("/api/v1")
- L591 · app.get("/*")
- L592 · app.get("/assets/*")
- L61 · isTurnstileEnabled calls (conditional paths may differ): Boolean
- L65 · parseRateLimitPerMinute calls (conditional paths may differ): Number.parseInt, Number.isFinite
- L73 · parsePositiveLimit calls (conditional paths may differ): Number.parseInt, Number.isFinite, Math.min
- L85 · isSiteUnlocked calls (conditional paths may differ): request.headers.get, some, cookie.split, split, part.trim
- L97 · shouldBypassSiteGate calls (conditional paths may differ): pathname.startsWith, pathname.endsWith
- L331 · generateApiKey calls (conditional paths may differ): chars.charAt, Math.floor, Math.random
Environment references: env.TURNSTILE_KEY · env.TURNSTILE_SECRET · env.API_RATE_LIMIT_PER_MINUTE · env.PASSWORD · c.env.TURNSTILE_SECRET · c.env.EMAIL_DOMAIN · c.env.DB · c.env.MAILBOX_TOKEN_SECRET · c.env.SENDER_EMAIL · c.env.SEND_RATE_LIMIT_PER_MINUTE · c.env.SEND_IP_RATE_LIMIT_PER_MINUTE · c.env.RESEND_API_KEY · c.env.MAILCHANNELS_API_KEY · c.env.SEND_EMAIL · c.env.COOKIES_SECRET · c.env.TURNSTILE_KEY · c.env.PASSWORD · c.env.SHOW_AFF · env.DB · env.ASSETS
- L7 · insertEmail calls (conditional paths may differ): execute, values, db.insert, console.error
- L15 · getEmails calls (conditional paths may differ): execute, from, db.select
- L24 · findEmailById calls (conditional paths may differ): execute, where, from, db.select, and, eq
- L45 · getEmailsByMessageTo calls (conditional paths may differ): orderBy, where, from, db.select, eq, desc, query.limit, query.execute
- L67 · getMailboxMetaByAddress calls (conditional paths may differ): Promise.all, execute, where, from, db.select, count, eq, limit, orderBy, desc, createdAt.toISOString, console.error
- L99 · getEmailsCount calls (conditional paths may differ): from, db.select, count
- L109 · deleteEmails calls (conditional paths may differ): where, db.delete, inArray, console.error
- L128 · deleteExpiredEmails calls (conditional paths may differ): execute, where, db.delete, lt, console.error
- L147 · findApiKeyByKey calls (conditional paths may differ): execute, where, from, db.select, eq, console.error
- L164 · updateApiKeyLastUsed calls (conditional paths may differ): execute, where, set, db.update, eq, console.error
- L179 · insertApiKey calls (conditional paths may differ): execute, values, db.insert, console.error
- L194 · insertMailbox calls (conditional paths may differ): execute, values, db.insert, console.error
- L207 · findMailboxById calls (conditional paths may differ): execute, where, from, db.select, eq, console.error
- L224 · findMailboxByAddress calls (conditional paths may differ): execute, where, from, db.select, eq, console.error
- L241 · getMailboxMessages calls (conditional paths may differ): Promise.all, execute, offset, limit, orderBy, where, from, db.select, eq, orderFn, count, Math.ceil, console.error
- L282 · findMailboxMessage calls (conditional paths may differ): execute, where, from, db.select, and, eq, console.error
- L303 · deleteMailboxMessage calls (conditional paths may differ): execute, where, db.delete, and, eq, console.error
- L323 · getMailboxMessageCount calls (conditional paths may differ): execute, where, from, db.select, count, eq, console.error
- L343 · deleteExpiredMailboxes calls (conditional paths may differ): execute, where, db.delete, lt, console.error
- L364 · getSiteStats calls (conditional paths may differ): execute, where, from, db.select, eq, console.error
- L381 · initSiteStats calls (conditional paths may differ): getSiteStats, execute, values, db.insert, console.error
- L402 · incrementEmailsReceived calls (conditional paths may differ): initSiteStats, execute, where, set, db.update, eq, console.error
- L421 · incrementAddressesCreated calls (conditional paths may differ): initSiteStats, execute, where, set, db.update, eq, console.error
- L440 · incrementApiKeysCreated calls (conditional paths may differ): initSiteStats, execute, where, set, db.update, eq, console.error
- L456 · incrementApiCalls calls (conditional paths may differ): initSiteStats, execute, where, set, db.update, eq, console.error
- L472 · getDateKey calls (conditional paths may differ): slice, date.toISOString
- L476 · getDailyStatsByDate calls (conditional paths may differ): execute, where, from, db.select, eq, console.error
- L493 · upsertDailyStatsField calls (conditional paths may differ): getDateKey, execute, onConflictDoUpdate, values, db.insert
- L522 · incrementDailyAddressesCreated calls (conditional paths may differ): upsertDailyStatsField, console.error
- L534 · incrementDailyEmailsReceived calls (conditional paths may differ): upsertDailyStatsField, console.error
- L546 · incrementDailyApiCalls calls (conditional paths may differ): upsertDailyStatsField, console.error
- L558 · incrementDailyApiKeysCreated calls (conditional paths may differ): upsertDailyStatsField, console.error
- L570 · incrementAndGetApiRateWindowCount calls (conditional paths may differ): execute, returning, onConflictDoUpdate, values, db.insert, console.error
- L4 · getD1DB calls (conditional paths may differ): drizzle
- L11 · createOpenApiDisabledResponse calls (conditional paths may differ): JSON.stringify
- L29 · requireOpenApi calls (conditional paths may differ): isOpenApiEnabled, createOpenApiDisabledResponse, next
Environment references: env.ENABLE_OPENAPI
- L58 · bytesToBase64Url calls (conditional paths may differ): String.fromCharCode, replace, btoa
- L69 · base64UrlToBytes calls (conditional paths may differ): replace, value.replace, base64.padEnd, Math.ceil, atob, Uint8Array.from, character.charCodeAt
- L76 · importHmacKey calls (conditional paths may differ): crypto.subtle.importKey, textEncoder.encode
- L104 · isAllowedMailboxAddress calls (conditional paths may differ): toLowerCase, address.trim, normalizedAddress.lastIndexOf, normalizedAddress.slice, includes, filter, map, emailDomains.split, item.trim
- L122 · createMailboxToken calls (conditional paths may differ): Date.now, toLowerCase, address.trim, bytesToBase64Url, textEncoder.encode, JSON.stringify, crypto.subtle.sign, importHmacKey
- L145 · verifyMailboxToken calls (conditional paths may differ): Date.now, token.split, crypto.subtle.verify, importHmacKey, base64UrlToBytes, textEncoder.encode, JSON.parse, decode
- L190 · escapeHtml calls (conditional paths may differ): value.replace
- L215 · appendSenderAttribution calls (conditional paths may differ): senderAttribution, escapeHtml
- L227 · buildResendPayload calls (conditional paths may differ): getProviderSenderName, appendSenderAttribution
- L247 · buildMailChannelsPayload calls (conditional paths may differ): getProviderSenderName, appendSenderAttribution
- L268 · buildCloudflareMimeMessage calls (conditional paths may differ): createMimeMessage, mimeMessage.setSender, getProviderSenderName, mimeMessage.setRecipient, mimeMessage.setSubject, mimeMessage.setHeader, mimeMessage.addMessage, appendSenderAttribution, mimeMessage.asRaw
Environment references: env.MAILBOX_TOKEN_SECRET · env.SENDER_EMAIL · env.SEND_CHANNEL · env.RESEND_API_KEY · env.MAILCHANNELS_API_KEY · env.SEND_EMAIL
Environment references: c.env.DB · c.env.API_RATE_LIMIT_PER_MINUTE
- L101 · mailboxesRouter.post("/")
- L187 · mailboxesRouter.get("/:id")
- L227 · mailboxesRouter.get("/:id/messages")
- L278 · mailboxesRouter.get("/:id/messages/:messageId")
- L332 · mailboxesRouter.delete("/:id/messages/:messageId")
- L16 · generateRandomLocalPart calls (conditional paths may differ): Math.floor, Math.random, padStart, String, pick, num2, yearSuffix
Environment references: c.env.DB · c.env.EMAIL_DOMAIN
Build and deployment pipeline · 2 GitHub Actions workflows
Repository CI declarations, separate from runtime request processing. Job dependencies and conditions are shown as written; long commands are shortened with an ellipsis; a workflow file does not prove a recent successful run.
Triggers: pull_request, push
validate · no job dependencies declared
- Checkout
actions/checkout@v4 - Setup pnpm
pnpm/action-setup@v4 - Setup Node.js
actions/setup-node@v4 - Install dependencies
pnpm install --frozen-lockfile - Test
pnpm test - Typecheck
pnpm typecheck - Build
pnpm build - Validate Worker bundle
pnpm exec wrangler deploy --dry-run - Check patch whitespace
git diff --check "$(git merge-base origin/${{ github.base_ref }} HEAD)" HEADCondition: github.event_name == 'pull_request'
Triggers: push, workflow_dispatch
Check for Cloudflare Credentials · no job dependencies declared
- Check for Cloudflare credentials
if [ -z "${{ secrets.CF_API_TOKEN }}" ] || [ -z "${{ secrets.CF_ACCOUNT_ID }}" ]; then echo "Cloudflare credentials not found, skipping deployment." echo "has_creds=false" >> $GITHUB_OUTPUT else echo "Cloudflare credentials found." echo "has_creds=true" >> $GITHUB_OUTPUT fi
Deploy · after check_credentials
Condition: needs.check_credentials.outputs.has_creds == 'true'
- Checkout
actions/checkout@v4 - Setup pnpm
pnpm/action-setup@v4 - Setup Node.js
actions/setup-node@v4 - Install dependencies
pnpm install --frozen-lockfile - Test
pnpm test - Typecheck
pnpm typecheck - Build
pnpm run build - Configure Wrangler
sed -i "s#\${D1_DATABASE_ID}#${D1_DATABASE_ID}#g" wrangler.toml sed -i "s#\${D1_DATABASE_NAME}#${D1_DATABASE_NAME}#g" wrangler.toml sed -i "s#\${EMAIL_DOMAIN}#${EMAIL_DOMAIN}#g" wrangler.toml sed -i "s#\${COOKIES_SECRET}#${COOKIES_SECRET}#g" wrangler.toml sed -i "s#\${TURNSTILE_KEY}#${TURNSTILE_KEY}#g" wrangler.toml sed -i "s#\${TURNSTILE_SECRET}#${TURNSTILE_SECRET}#g" wrangler.toml if [ -n "${SEND_CHANNEL}" ]; then case "${SEND_CHANNEL}" in resend) [ -n "${RESEND_API_KEY}" ] || { echo "RESEND_… - Apply D1 Migrations
npx wrangler d1 migrations apply ${{ secrets.D1_DATABASE_NAME }} --remote - Deploy
cloudflare/wrangler-action@v3 - Configure sender secrets
if [ -z "${SEND_CHANNEL}" ]; then exit 0 fi printf '%s' "${MAILBOX_TOKEN_SECRET}" | pnpm exec wrangler secret put MAILBOX_TOKEN_SECRET if [ -n "${RESEND_API_KEY}" ]; then printf '%s' "${RESEND_API_KEY}" | pnpm exec wrangler secret put RESEND_API_KEY fi if [ -n "${MAILCHANNELS_API_KEY}" ]; then printf '%s' "${MAILCHANNELS_API_KEY}" | pnpm exec wrangler secret put MAILCHANNELS_API_KEY fi
build: pnpm --filter frontend run builddeploy: pnpm build && wrangler deploy
build: vite build
Repository README
View original on GitHub ↗Full upstream document by @oiov · README.md · snapshot e78cb52
🌟 推荐 Claude Code 稳定 API 渠道:Nbility AI Gateway ,支持 Claude Fable 5、GPT 5.6 等主流 AI Coding 大模型
🌈 特点
- 🎯 隐私友好,无需注册,开箱即用
- ✈️ 支持邮件收发
- ✨ 支持保存密码,找回邮箱
- 😄 支持多域名后缀
- 🔌 开放 RESTful API,支持程序化调用
- 🚀 快速部署,纯 Cloudflare 方案,无需服务器
原理:
- Email worker 接收电子邮件
- 前端 (Vite + React) 显示电子邮件
- 邮件存储 (Cloudflare D1)
- 发信使用 Resend API、MailChannels API 或 Cloudflare 原生发件
👋 自部署教程
本项目基于 Cloudflare Workers 和 Cloudflare D1 构建。您只需要一个托管在 Cloudflare 上的域名即可。
准备工作
- Cloudflare 账户与托管在 Cloudflare 上的域名
- 本地安装 Node.js 环境(版本 >= 22.x)和 pnpm
自动部署 (推荐)
本项目已包含一个预先配置好的 GitHub Action 工作流,可以帮助您自动将 Vmail 应用部署到 Cloudflare。
详细步骤请参考 GitHub Action 自动部署教程。
手动部署步骤
克隆项目到本地
git clone https://github.com/oiov/vmail cd vmail pnpm install创建 Cloudflare D1 数据库 在 Cloudflare 控制台或使用 Wrangler CLI 创建一个 D1 数据库。
配置
wrangler.toml将根目录下的wrangler.toml文件中的${...}占位符替换为您的 Cloudflare 和 D1 配置信息。您也可以通过 Cloudflare Pages 的环境变量来设置这些值。构建和部署
# 构建前端应用 pnpm run build # 部署到 Cloudflare pnpm run deployWrangler 将会自动处理前端静态资源和 Worker 的部署,并根据配置应用数据库迁移。
配置电子邮件路由 在您的 Cloudflare 域名管理界面,进入
Email->Email Routing->Routes,设置一个Catch-all规则,将所有发送到您域名的邮件Send to a Worker,选择您刚刚部署的 Worker。
环境变量
通过 GitHub Actions 部署到 Cloudflare Workers 时,您需要配置以下环境变量:
D1_DATABASE_NAME: 您的 D1 数据库名称。D1_DATABASE_ID: 您的 D1 数据库 ID。COOKIES_SECRET: 用于签名 Cookie 的密钥。EMAIL_DOMAIN: 您的邮箱域名,例如example.com,example.net。TURNSTILE_KEY: 您的 Turnstile 站点密钥,可选。TURNSTILE_SECRET: 您的 Turnstile 密钥,可选。PASSWORD: 站点访问密码(可选)。API_RATE_LIMIT_PER_MINUTE: API 每分钟请求限制(可选,默认 100)。SEND_CHANNEL: 发件渠道,可选resend、mailchannels、cloudflare;不配置时隐藏发信功能。旧值send_email仍兼容,但已弃用。SENDER_EMAIL: 固定的发件地址,必须是服务商允许或已验证的地址;临时邮箱仅作为Reply-To。MAILBOX_TOKEN_SECRET: 邮箱发信授权令牌的签名密钥,启用发信时必填,并应通过 Wrangler secret 配置。RESEND_API_KEY: Resend API 密钥,仅SEND_CHANNEL=resend时需要,通过 Wrangler secret 配置。MAILCHANNELS_API_KEY: MailChannels API 密钥,仅SEND_CHANNEL=mailchannels时需要,通过 Wrangler secret 配置。SEND_RATE_LIMIT_PER_MINUTE: 每个邮箱每分钟最大发信数(可选,默认 3)。SEND_IP_RATE_LIMIT_PER_MINUTE: 每个 IP 每分钟最大发信数(可选,默认 10)。SHOW_AFF: 是否显示推广弹窗和链接(可选,设置为true开启)。ENABLE_OPENAPI: 是否开启 OpenAPI 调用功能(可选,默认false;只有显式设置为true时才允许创建 API Key 和访问/api/v1/*)。
发信密钥不要写入 wrangler.toml,请使用:
pnpm exec wrangler secret put MAILBOX_TOKEN_SECRET
pnpm exec wrangler secret put RESEND_API_KEY # Resend 时
pnpm exec wrangler secret put MAILCHANNELS_API_KEY # MailChannels 时
使用 Cloudflare Worker 原生发信时,将 SEND_CHANNEL 设置为 cloudflare,并配置 SENDER_EMAIL 和 MAILBOX_TOKEN_SECRET;不需要 RESEND_API_KEY 或 MAILCHANNELS_API_KEY。此外,需要在该域名上启用 Cloudflare Email Routing。wrangler.toml 中名为 SEND_EMAIL 的 [[send_email]] 绑定是 Cloudflare 固定配置,无需改名。
🔨 本地运行调试
复制环境变量文件
# 此命令会创建一个本地环境变量文件,wrangler dev 会自动加载 cp .env.example .env填写本地环境变量 在
.env文件中填写必要的环境变量,特别是D1_DATABASE_ID等。您需要先在 Cloudflare 创建一个 D1 数据库用于本地开发。启动开发服务器
pnpm run dev该命令会同时启动前端 Vite 开发服务器和本地的 Wrangler Worker 环境。
📖 API 文档
Vmail 提供完整的 RESTful API,支持通过程序化方式创建临时邮箱、查询收件箱。
获取 API Key
访问 API 文档页面 创建免费的 API Key。
API 端点
| 方法 | 端点 | 说明 |
|---|---|---|
POST |
/api/v1/mailboxes |
创建临时邮箱 |
GET |
/api/v1/mailboxes/:id |
获取邮箱信息 |
GET |
/api/v1/mailboxes/:id/messages |
获取收件箱(支持分页) |
GET |
/api/v1/mailboxes/:id/messages/:messageId |
获取邮件详情 |
DELETE |
/api/v1/mailboxes/:id/messages/:messageId |
删除邮件 |
快速开始
# 1. 创建临时邮箱
curl -X POST https://vmail.dev/api/v1/mailboxes \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json"
# 响应: { "data": { "id": "abc123", "address": "random@domain.com", ... } }
# 2. 查询收件箱
curl https://vmail.dev/api/v1/mailboxes/abc123/messages \
-H "X-API-Key: your-api-key"
# 3. 获取邮件详情
curl https://vmail.dev/api/v1/mailboxes/abc123/messages/msg_001 \
-H "X-API-Key: your-api-key"
完整文档请访问:https://vmail.dev/api-docs
📝 License
GNU General Public License v3.0
Star History
Frequently asked about Vmail
What is Vmail?+
Vmail is a self-hosted Mailinator/Temp Mail alternative built on the Cloudflare developer platform. Disposable custom-domain inboxes with a browser interface and optional sending.
What does Vmail replace?+
Vmail is listed as an alternative to Mailinator, Temp Mail. Compare the features and tradeoffs before migrating.
What Cloudflare primitives does Vmail use?+
Vmail is built on D1, Email Workers, Workers.
How much does Vmail cost to run?+
The reviewed Cloudflare deployment is eligible for Free-plan allowances for the stated small workload and feature scope. Usage limits, CPU, required account setup and separate services apply. Free hosting scope is receiving only with SEND_CHANNEL unset; outbound email is hidden until separately configured. Provide an owned domain on Cloudflare and catch-all Email Routing; registration/domain cost is separate from Worker hosting. Replace all template placeholders and cookie/access secrets, keep Worker CPU/requests and D1 rows/storage below Free limits. API access is off by default until ENABLE_OPENAPI is true; any sending requires provider account restrictions, verified sender and its own pricing review. The active email-sending binding is observed but its presence does not prove free delivery to arbitrary recipients. Workers Free dynamic requests are shared across this account (100,000/day), with 10 ms CPU per invocation; workload fit is conditional and has not been measured. D1 Free allowance: 5 million rows read/day, 100,000 rows written/day and 5 GB total storage; unindexed scans and history retention consume quota. Check current Cloudflare pricing before deploying.
Is Vmail open source?+
The upstream repository declares the GPL-3.0 license. Read its terms at https://raw.githubusercontent.com/oiov/vmail/e78cb52ad450116b2c7bb174894ad2f3a6acf927/LICENSE. Source code and contributor credit are available at https://github.com/oiov/vmail.



Discussion · 0
sign in to comment →