mail2telegram: an email inbox inside Telegram, hosted on Cloudflare Workers
Receive email in Telegram: instant push notifications plus a Mini App inbox.
At a glance
- What is it?
- mail2telegram is a Telegram bot that receives email through Cloudflare Email Routing and pushes it to your chat, with a Mini App providing the inbox, attachments, replies and settings, all running serverless on Workers with D1 and R2. Version 2.0 moved every setting into the Mini App, which is a better design and a harder migration.
- Who is it for?
- mail2telegram fits a person who wants a personal, disposable email address reachable from a phone without opening a mail client, and it fits a team already committed to Cloudflare who wants to avoid running a mail server.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 3 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Two surfaces for one mailbox: a push and a Mini App
The project is a Telegram bot for receiving email, running entirely on Cloudflare Workers, and its central idea is that email is delivered through two different surfaces depending on what you want to do with it.
The first surface is the push notification. Every incoming email arrives in your chat as a message with quick action buttons. The second is a Telegram Mini App, which is a web application that carries the full history, the attachments and every setting. The documentation describes it as instant push notifications plus a Mini App inbox.
The data flow is drawn in a two line diagram in the README:
Email ──▶ Telegram push with quick action buttons
└─▶ Mini App inbox (history, attachments, settings)That split is the design decision that makes the thing pleasant to use. Telegram is a good notification surface and a poor document reader, so the bot uses it as a signal and keeps the reading surface in a Mini App, which is a real web application with folders, search and a reading pane.
The Mini App opens from the bot with /start, and the first /start from a chat also binds the webhook and points the bot menu button at the worker, so later opens can use the menu button directly. Inside, there are three areas. The inbox lists history per folder, with Inbox, Spam, Trash and Sent, plus search, unread and starred filters and a Load more control that pulls through history. The reader is a sandboxed HTML view with a plain text toggle, attachments, star, read and delete actions, an AI summary and a reply button. The settings cover the white list, block list, an address tester, the block policy, forwarding, summary options and mail handling limits.
The layout is responsive in a specific way. On phones the app uses a native tab bar and navigation stack. On desktop clients, macOS and Telegram Desktop, and on wide viewports, it switches to an iPad-style split view with a sidebar, a list and a reading pane.
The push message has three buttons and one hard character limit
The push message structure is documented and, in version 2, unchanged, which is a good sign for a project that reorganised everything else around it. The format is:
[Subject]
-----------
From : [sender]
To : [recipient]
(Preview)(Summary)(Open)Each button does something different. Preview shows the plain text body directly in the chat, and it is limited to 4096 characters, which is a Telegram limit rather than a design choice. Summary appears only when a summary backend has been enabled in Settings, either Workers AI or an OpenAI-compatible provider, so on a deployment without a key the button is simply absent. Open launches the Mini App directly to that message's detail page.
There is a constraint on the third one. Telegram only allows Mini App buttons in private chats, so group notifications omit the Open button. That is a platform rule rather than a missing feature, and it is the kind of thing that only surfaces when you try to add the bot to a group and wonder why the button is missing.
The last feature of the push path is the one that is easy to miss. You can reply to any pushed message in Telegram to answer the sender, and that reply goes out through Resend. So the round trip is complete: mail arrives in the chat, you answer it in the chat, and the reply reaches the sender without you opening anything.
That reply capability is why the RESEND_API_KEY binding is described as optional. Without it you get a read only mailbox plus the AI summary, and with it the Mini App can also compose new mail.
The CPU budget decides the attachment size you can afford
There is a specific, quantified constraint in the documentation that tells you how to size the deployment, and it is the first thing to read before pointing a real address at it.
Parsing a message costs roughly 2 milliseconds of CPU per megabyte, so on the Workers free plan, which allows 10 milliseconds of CPU per request, Max Size should be kept under about 2 megabytes. The paid plan's 30 second budget has no such constraint.
So the free tier is not just a smaller quota, it is a different operating envelope. A 2 megabyte message will consume the entire free request budget in parsing alone, which is why the guidance is to keep the limit under 2 megabytes rather than exactly at it.
Attachments follow from this, and the rules are stated in detail. Attachments are stored in R2 and listed in the reader, where they can be downloaded. If no BUCKET binding is configured, mail is still stored without attachment contents, so the deployment works without object storage, just without files.
Attachments need the whole message, so Max Size in Mail Handling has to be at least as large as the mail you want to receive. Base64 encoding adds about a third to every attachment, which means the limit has to be budgeted around the largest file you expect rather than the size you think of. Mail over the limit has its body truncated, and because attachments follow the body in the message, a truncated mail stores no attachments at all. The reasoning is given, and it is sound: the alternative would be handing you a corrupt download.
There is a second route around the problem. The oversize policy Headers skips the body altogether, so you get the headers and no content, which is a reasonable setting for a mailbox where you mostly need to know something arrived and then fetch the real thing elsewhere.
Retention is a separate setting and has a narrower meaning than it first appears. Mail older than the retention setting is not part of the notification cache, but the full history remains until you delete it from the Mini App. Retention prunes the cache, not the mailbox.
Access control is Telegram initData plus a browser password
The security model is described precisely enough to be useful, which is not always the case for a bot that handles email.
The Mini App is protected by Telegram initData validation against TELEGRAM_TOKEN, and it is restricted to the IDs in TELEGRAM_ID. The initData check is the meaningful one, because it verifies that the request genuinely came from Telegram with your bot token, so a browser cannot forge a valid session by setting a cookie.
In a plain browser, the same URL asks for WEB_PASSWORD, and when no password is configured the Mini App is the only way in and the project landing page is shown instead. That default is the right one. It means an unauthenticated deployment is not an open mailbox, it is a deployment that only Telegram can reach.
The TELEGRAM_ID binding accepts comma separated chat ids, and the documentation notes that groups start with -100. That connects to the earlier constraint: group notifications exist, they just do not get the Open button, so the Mini App is still the way to read a message that arrived in a group chat.
The address rules are the other half of the access story, and they live in the Mini App rather than in configuration. A rule matches either an exact address, case-insensitively, or a regular expression. The white list takes precedence over the block list, so an allow rule can override a broad block rule for the same address.
That precedence is worth pausing on, because it is the kind of ordering that matters in a filter. A block list of ten domains with a white list that names one address inside a blocked domain is a legitimate configuration, and it works because the allow list is checked first. The settings also include a block policy, forwarding, and an address tester for checking whether a rule matches.
Version 2 moved the configuration out of variables and into D1
The current release is 2.0.0, dated 2026-09-15, and it is a substantial restructure rather than an increment. The most important line in the documentation is that all behavior is configured in the Mini App and stored in D1, and that the worker itself only needs a few variables for its Telegram identity and optional API keys.
That is a good architectural decision. A worker that reads its configuration from a database can be redeployed without reconfiguring it, and the person doing the configuring uses a form rather than editing a wrangler configuration file. It also means the same worker binary can serve several configurations.
It also makes the upgrade from 1.0 harder, and the project is upfront about that. There is a separate Migration Guide that covers what changed in 2.0, the step-by-step upgrade path and the variable mapping, and the Deployment Guide covers installing 2.0 from scratch.
The worker variables that remain are few and are described in the package manifest as Cloudflare bindings. TELEGRAM_TOKEN is the bot token from BotFather, obtained with /newbot. TELEGRAM_ID is the destination chat ids, comma separated, with a pointer to userinfobot for finding yours and a note that groups start with -100. WEB_PASSWORD is the optional password for opening the Mini App in a plain browser outside Telegram, and leaving it empty allows only the Telegram Mini App. RESEND_API_KEY is the optional Resend key that enables replying and composing. OPENAI_API_KEY is the optional OpenAI-compatible key used as the default summary backend, and it can also be set later in the Mini App settings.
The project ships three deployment guides: a quick deploy that clones the repo, creates the D1 and R2 resources, asks for the Telegram values and deploys through a Cloudflare Workers Builds button; the full deployment guide covering bot setup, storage, the button deployment, Cloudflare Workers Builds, GitHub Actions or the CLI, runtime variables and Email Routing; and the migration guide.
The manifest also declares the Cloudflare products the deployment needs, which are Workers, D1 and R2.
A pnpm workspace with the wire contract in its own package
The repository is a pnpm workspace with three packages and the deployable worker configuration at the root, and the layout is documented in a table that is worth reading as an architecture description.
The server package is the Cloudflare Worker, holding the email handlers, the Telegram bot, the Mini App API, the D1 migrations and the Worker-runtime tests. The web package is the Telegram Mini App itself, built with Vite and React, and built into packages/web/dist/client. The shared package is types only, described as the HTTP contract consumed by both sides, and it owns every type that crosses the wire.
That third package is the interesting one. Putting the wire contract in its own types-only package means the worker and the Mini App cannot disagree about a request shape without the type checker noticing, and it means neither side imports the other. The consequence for a contributor is that changing an API endpoint means changing the shared types and both packages, which is more work than a quick fix and considerably less guesswork later.
The development commands are all in the root package.json:
pnpm install
pnpm dev # Vite dev server on :5173 + wrangler dev on :8787
pnpm build # typecheck every package and build the Mini App into packages/web/dist/client
pnpm typecheck # tsc --noEmit for each package
pnpm test # pure-logic tests (tsx) + Worker-runtime tests (vitest, Miniflare)The build script typechecks every package before building the Mini App, so a type error in the shared contract fails the build rather than reaching runtime.
The test setup is split in a way that mirrors the architecture. There are pure-logic tests run with tsx, and Worker-runtime tests run with vitest against Miniflare, which is how you test code that depends on D1 and the inbound email API without deploying. The scripts separate them further, with test:unit covering mergeSettings, testAddress and parseEmail, and test:pool covering the D1, inbound-email and cleanup tests in the Workers runtime. Linting uses oxlint with an oxfmtrc configuration for formatting.
Other things in the tree worth knowing about: a .dev.vars.example file for local secrets, a wrangler.jsonc for the worker configuration, a pnpm-workspace.yaml, a tsconfig.json, a scripts directory and a docs directory holding the deploy and migration guides in both English and Chinese.
Where this is the wrong shape of tool
Three boundaries are worth stating plainly.
The first is that this is not a mail client. The Mini App has an inbox with folders, a reader, a search box, starred and unread filters, attachments and a reply action. It does not have threading, multiple accounts, rules, signatures, drafts, search operators, or a local copy of your message store, and it does not sync with anything else. If the requirement is a mail application, this is not it, and no amount of configuration will make it one.
The second is the dependency chain. Getting mail into Telegram this way means Cloudflare Email Routing forwarding every message to a worker, the worker storing the message in D1, attachments in R2, and replies going out through Resend. That is four services and two credentials beyond the Telegram bot, and a deployment that needs Resend only becomes a two-way mailbox. A user who only wants to be notified of mail and read it elsewhere does not need Resend, and the documentation is clear that those bindings are optional.
The third is the fragility of the routing. The whole path depends on Cloudflare Email Routing continuing to forward to a worker, on the worker continuing to be a viable place to parse an email, and on Telegram continuing to support the Mini App surfaces the project depends on. All three are outside the project's control, and a change in any of them breaks the delivery path with no local fix. That is inherent to the design rather than a defect in it, and it is the cost of a zero server, zero maintenance mailbox.
The AI summary is the smallest of the three concerns but the one most likely to surprise. The Summary button appears only when a summary backend is enabled, and it sends the message content to Workers AI or to whatever OpenAI-compatible provider you configured. On a mailbox with password reset emails and account notifications, that is a meaningful disclosure, and the setting lives in the Mini App where it is easy to enable without thinking about it.
Editorial conclusion
mail2telegram fits a person who wants a personal, disposable email address reachable from a phone without opening a mail client, and it fits a team already committed to Cloudflare who wants to avoid running a mail server. It is the wrong choice if you need a general purpose mail client, since the inbox is a Mini App with a fixed feature set rather than a mail application, and it is the wrong choice on the free Workers plan if you expect large attachments, because the documentation ties the size limit to the CPU budget. Before deploying it, set Max Size deliberately, because the documentation explains that a truncated message stores no attachments at all, and read the migration guide if you are upgrading a 1.0 deployment, since 2.0 moved configuration out of worker variables and into D1.
Frequently asked questions
What is mail2telegram and how does it receive email?
It is a Telegram bot for receiving email that runs entirely on Cloudflare Workers. Mail arrives through Cloudflare Email Routing, which forwards every message to the worker, and the worker pushes a notification to your chat with quick action buttons while a Telegram Mini App holds the history, attachments and settings.
How do I deploy mail2telegram?
The quick deploy path clones the repo, creates the D1 and R2 resources, asks for the Telegram values and deploys through the Deploy to Cloudflare button. The deployment guide covers installing 2.0 from scratch, including Telegram bot setup, storage, runtime variables and Email Routing binding.
How do I reply to an email from mail2telegram?
You reply to any pushed message in Telegram and the answer goes out to the sender through Resend. That requires the optional RESEND_API_KEY binding, and the same key also lets the Mini App compose new mail.
What is the size limit for attachments in mail2telegram?
Parsing costs roughly 2ms of CPU per MB, so on the free Workers plan with a 10ms CPU budget Max Size should stay under about 2MB. Base64 encoding adds about a third to each attachment, and a mail over the limit is truncated, which means it stores no attachments at all.
How do I upgrade mail2telegram from 1.0 to 2.0?
Version 2.0 moved all behavior out of worker variables and into the Mini App, with the settings stored in D1. The repository has a separate migration guide covering what changed, the step-by-step upgrade path and the variable mapping.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/tbxark-mail2telegram)Was this useful?