Setting up the Telegram adapter
Mail gets delivered as a Telegram message to whoever owns the address. deploy-cloudflare.md only gets mail as far as received and stored, so finish that first.
What you need
- A cloned repo with
npm installrun in it. - A deployed Worker (the same one from
deploy-cloudflare.md, or a fresh one if this is the only adapter you're running).
1. Create a bot
Message @BotFather on Telegram, /newbot, follow the prompts. You get back a bot token.
2. Pick a webhook secret
Not something Telegram gives you -- make one up yourself, any random string (openssl rand -hex 32 works). Telegram echoes it back on every webhook call so a request can be confirmed as actually coming from Telegram, not just anyone who finds the URL.
3. Set your secrets
npx wrangler secret put TELEGRAM_BOT_TOKEN
npx wrangler secret put TELEGRAM_WEBHOOK_SECRET
Or from the dashboard: Workers & Pages → your Worker → Settings → Variables and Secrets → Add, Type: Secret, one entry per name above. Not Type: Text — that's a plaintext variable that gets silently wiped on this Worker's next deploy, since only ADAPTERS is declared in wrangler.jsonc and a redeploy makes that file the source of truth for anything not a proper Secret.
Add "telegram" to ADAPTERS in wrangler.jsonc's vars (comma-separated if Discord's there too: "discord,telegram"), then redeploy:
npx wrangler deploy
Your webhook path is /telegram-webhook on that same Worker.
Running both Discord and Telegram and want them on separate Workers instead?
Not needed for a normal setup — one Worker handling everything is the default for a reason, nothing extra to deploy or keep in sync. This only matters if you specifically want Discord and Telegram bundle-isolated from each other, so a Discord-only change can't bloat what Telegram's Worker ships, and vice versa.
npx wrangler secret put TELEGRAM_BOT_TOKEN --config wrangler.telegram.jsonc
npx wrangler secret put TELEGRAM_WEBHOOK_SECRET --config wrangler.telegram.jsonc
npx wrangler deploy --config wrangler.telegram.jsonc
If your main Worker is in domain mode, also set DISPOSABLE_DOMAIN on this one to match — Cloudflare doesn't share secrets between Workers, so this has to be kept in sync by hand:
npx wrangler secret put DISPOSABLE_DOMAIN --config wrangler.telegram.jsonc
Skip that and this Worker falls back to mail.tm mode regardless of what the main Worker does, since it has no way to know otherwise.
Webhook path is /webhook on this second Worker (not /telegram-webhook), and it needs TELEGRAM_BOT_TOKEN set on the main Worker too, so it can deliver inbound mail to Telegram users — that part always runs on whichever Worker owns Email Routing and the cron, regardless of which Worker handles the webhook itself.
4. Point Telegram at your endpoint
curl "https://api.telegram.org/bot<TOKEN>/setWebhook?url=<your-worker-url>/telegram-webhook&secret_token=<your-webhook-secret>"
(/webhook instead of /telegram-webhook if you split onto a second Worker.) A successful response looks like {"ok":true,"result":true,"description":"Webhook was set"}.
5. Try it
Message your bot /new. Group chats are refused: commands only work in a private chat with the bot, since Telegram has no way to send a reply that's visible only to the person who ran the command, the way a reply in a private chat already is for everyone else in it.
Commands
Plain text after the command, no structured options, everything is just typed as one string:
| Command | What it does | Rate limit |
|---|---|---|
/new [expiry] [note] |
Creates an address. Permanent unless expiry is given. |
1 per 30s |
/list |
Your addresses, notes, and expiry. | 15 per 60s |
/extend <address> [expiry] |
Changes when an address expires. | 15 per 60s |
/note <address> [note] |
Labels an address. Blank clears it. | 15 per 60s |
/torch <address> |
Revokes an address. | 15 per 60s |
/remind [on|off] |
Expiry reminder messages. Blank shows the current setting. | 15 per 60s |
For /new and /note, a leading number is read as expiry; everything else is the note. For /extend, the address comes first and an optional trailing number is the new expiry:
/new permanent, no note
/new 7 expires in 7 days
/new 7 netflix signup expires in 7 days, noted
/new netflix signup permanent (no leading number), noted
/extend x@you.com 10 days from now
/extend x@you.com 5 5 days from now
/extend x@you.com 0 permanent
expiry is in days on both /new and /extend, 0 meaning permanent. One asymmetry: bare /new is permanent, bare /extend uses the 10 day default, since /extend should do what its name says. /extend also sets expiry relative to now rather than adding to what's left: an address with 8 days left extended by 5 has 5 days, not 13.
Permanent addresses still count against your active-address limit, and show as permanent in /list rather than a countdown. Cleanup skips them, so /torch is what ends one.
Notes
A random local part tells you nothing about what you used it for. A note is an optional label, up to 80 characters, shown in /list, visible only to the address's owner:
/new 0 netflix trial
/note x7k2p9qzrm@you.com bank alerts
/note x7k2p9qzrm@you.com clears it
Expiry reminders
Off until asked for:
/remind on message about a day before an address expires
/remind off stop
/remind current setting
One message covering everything of yours expiring soon, with notes, so you can /extend what you still need. It rides the daily cleanup cron, so it lands 24 to 48 hours ahead rather than exactly a day; an address living under about two days never gets one, since no run sees it with a day still left: /new 1 won't warn. /extend re-arms the reminder against the new expiry.
Defaults for all of the above are configurable, see configuration.md.