Cloudsteading
Vmail temporary inbox application with an empty message list

Vmail

Disposable custom-domain inboxes with a browser interface and optional sending.

Vmail is a self-hosted Mailinator/Temp Mail alternative built on Cloudflare (D1, Email Workers, Workers). Free tier eligible within limits. Inspect the source and license in the linked repository.

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

@oiov

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.
Check current pricing ↗
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.

Mailinator logoMailinator ↗

Editorial workflow alternative: Temporary mailbox receiving and optional programmatic inbox APIs; no enterprise testing, public-inbox or throughput parity claim.

See supporting source ↗
external SaaS target
varies
→ D1 + Email Workers + Workers
external SaaS target
varies
→ D1 + Email Workers + Workers

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 ↗
Public interface
Configured entry points1
vmail
wrangler.toml
↓
App
vmail
entry
Cloudflare Workers
Entrypoint: worker/src/index.tsConfigured cron (UTC): 0 * * * *
↓

Configuration and workflow sources

Reviewed commit e78cb52ad450. Files were read as data; upstream applications and CI jobs were not executed.

Deployment configuration · 1 files
wrangler.toml ↗

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 → D1
  • SEND_EMAIL → Send Email
  • ASSETS → 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.

worker/src/index.ts ↗
  • 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

worker/src/database/dao.ts ↗
  • 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
worker/src/database/db.ts ↗
  • L4 · getD1DB calls (conditional paths may differ): drizzle
worker/src/utils.ts ↗
  • L8 · encrypt calls (conditional paths may differ): String.fromCharCode, text.charCodeAt, secret.charCodeAt, btoa
  • L24 · decrypt calls (conditional paths may differ): atob, String.fromCharCode, text.charCodeAt, secret.charCodeAt
worker/src/api/v1/index.ts ↗
  • L11 · v1.use("/*")
  • L14 · v1.use("/*")
  • L17 · v1.use("/*")
  • L20 · v1.route("/mailboxes")
worker/src/openapi.ts ↗
  • L11 · createOpenApiDisabledResponse calls (conditional paths may differ): JSON.stringify
  • L29 · requireOpenApi calls (conditional paths may differ): isOpenApiEnabled, createOpenApiDisabledResponse, next

Environment references: env.ENABLE_OPENAPI

worker/src/sender.ts ↗
  • 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

worker/src/api/v1/middleware/apiKeyAuth.ts ↗

    Environment references: c.env.DB · c.env.API_RATE_LIMIT_PER_MINUTE

    worker/src/api/v1/routes/mailboxes.ts ↗
    • 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.

    CI · .github/workflows/ci.yml ↗

    Triggers: pull_request, push

    validate · no job dependencies declared

    1. Checkoutactions/checkout@v4
    2. Setup pnpmpnpm/action-setup@v4
    3. Setup Node.jsactions/setup-node@v4
    4. Install dependenciespnpm install --frozen-lockfile
    5. Testpnpm test
    6. Typecheckpnpm typecheck
    7. Buildpnpm build
    8. Validate Worker bundlepnpm exec wrangler deploy --dry-run
    9. Check patch whitespacegit diff --check "$(git merge-base origin/${{ github.base_ref }} HEAD)" HEADCondition: github.event_name == 'pull_request'
    Deploy to Cloudflare · .github/workflows/deploy.yml ↗

    Triggers: push, workflow_dispatch

    Check for Cloudflare Credentials · no job dependencies declared

    1. Check for Cloudflare credentialsif [ -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'

    1. Checkoutactions/checkout@v4
    2. Setup pnpmpnpm/action-setup@v4
    3. Setup Node.jsactions/setup-node@v4
    4. Install dependenciespnpm install --frozen-lockfile
    5. Testpnpm test
    6. Typecheckpnpm typecheck
    7. Buildpnpm run build
    8. Configure Wranglersed -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_…
    9. Apply D1 Migrationsnpx wrangler d1 migrations apply ${{ secrets.D1_DATABASE_NAME }} --remote
    10. Deploycloudflare/wrangler-action@v3
    11. Configure sender secretsif [ -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
    package.json ↗
    • build: pnpm --filter frontend run build
    • deploy: pnpm build && wrangler deploy
    frontend/package.json ↗
    • build: vite build

    Full upstream document by @oiov · README.md · snapshot e78cb52

    yesmore%2Fvmail | Trendshift

    𝐕𝐌𝐀𝐈𝐋.𝐃𝐄𝐕

    部署教程 · AI帮你部署 · API 文档 · English | 简体中文

    使用 Cloudflare Email Worker 实现的临时电子邮件服务

    🌟 推荐 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 自动部署教程。

    手动部署步骤

    1. 克隆项目到本地

      git clone https://github.com/oiov/vmail
      cd vmail
      pnpm install
      
    2. 创建 Cloudflare D1 数据库 在 Cloudflare 控制台或使用 Wrangler CLI 创建一个 D1 数据库。

    3. 配置 wrangler.toml 将根目录下的 wrangler.toml 文件中的 ${...} 占位符替换为您的 Cloudflare 和 D1 配置信息。您也可以通过 Cloudflare Pages 的环境变量来设置这些值。

    4. 构建和部署

      # 构建前端应用
      pnpm run build
      
      # 部署到 Cloudflare
      pnpm run deploy
      

      Wrangler 将会自动处理前端静态资源和 Worker 的部署,并根据配置应用数据库迁移。

    5. 配置电子邮件路由 在您的 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 固定配置,无需改名。

    🔨 本地运行调试

    1. 复制环境变量文件

      # 此命令会创建一个本地环境变量文件,wrangler dev 会自动加载
      cp .env.example .env
      
    2. 填写本地环境变量 在 .env 文件中填写必要的环境变量,特别是 D1_DATABASE_ID 等。您需要先在 Cloudflare 创建一个 D1 数据库用于本地开发。

    3. 启动开发服务器

      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

    Star History Chart

    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 →
    No comments yet — be the first.