feat: email each new reminder via Cloudflare Email Routing - #9
Open
ofa1 wants to merge 3 commits into
Open
Conversation
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
mhmic-telegram-cron | e8adfb5 | Sep 06 2026, 08:38 PM |
There was a problem hiding this comment.
🟡 Changes recommended
Email-send failures can surface full recipient addresses in the notifications chat, which is a avoidable information disclosure risk.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds an email delivery channel (via Cloudflare SEND_EMAIL) for each newly posted reminder, while keeping Telegram as the primary channel and ensuring email failures don’t cause re-posts on the next cron tick.
Changes:
- Introduces
src/email.tsto compose and send reminder emails (per-recipient) and adds HTML/text sanitization helpers. - Updates handler flow so Telegram sends first, then email is attempted best-effort with failures reported to the notifications chat.
- Updates test setup (Vitest + Cloudflare pool), adds unit tests for email composition/sending, and refreshes related dependencies/config/docs.
File summaries
| File | Description |
|---|---|
| wrangler.jsonc | Adds SEND_EMAIL binding configuration. |
| vitest.config.ts | Removes prior workers config file. |
| vitest.config.mts | Adds new Vitest config using cloudflareTest() plugin and test bindings (incl. email secrets). |
| tsconfig.json | Updates Cloudflare Vitest pool types entry. |
| tests/unit/utils.test.ts | Adds coverage for htmlToText and escapeHtml. |
| tests/unit/handlers.test.ts | Mocks email sending and adds handler-level email behavior tests. |
| tests/unit/email.test.ts | Adds comprehensive unit tests for composing/sending reminder emails. |
| tests/env.d.ts | Removes custom cloudflare:test ProvidedEnv typing augmentation. |
| src/utils.ts | Adds htmlToText (via cheerio) and escapeHtml. |
| src/handlers.ts | Sends Telegram first, then best-effort email with failure notifications. |
| src/env.d.ts | Adds durable secret typings that survive wrangler types regeneration. |
| src/email.ts | New module to build and send reminder emails via SEND_EMAIL. |
| README.md | Documents email notification behavior and configuration. |
| package.json | Bumps dev tooling versions; adds/keeps cheerio dependency for HTML decoding. |
| package-lock.json | Updates lockfile for dependency/version bumps. |
| .env.example | Adds EMAIL_FROM / EMAIL_TO placeholders and notes. |
Review details
- Files reviewed: 12/17 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Moves to wrangler 4.129.0, @cloudflare/vitest-pool-workers 0.22.0 and vitest 4. The existing suite passes unchanged on the new toolchain. Three interface changes came with it: - The pool no longer exports ./config. Its options are now passed to the cloudflareTest() Vite plugin, and because the package is ESM only the config has to be loaded as ESM, hence vitest.config.mts. - cloudflare:test types moved to the ./types subpath, and ProvidedEnv is gone — env is typed as Cloudflare.Env directly, so tests/env.d.ts is obsolete. - Generated Env types changed shape. Secrets now live in src/env.d.ts and are merged into the generated interfaces. wrangler types rebuilds worker-configuration.d.ts from wrangler.jsonc alone, so secrets added to that file by hand are silently lost on every regeneration; they survive now. The lockfile was regenerated with npm 12 to work around an arborist crash in npm 10 while resolving vitest 4's peer graph. npm ci still installs it correctly on npm 10, which is what CI runs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UxmzyMHFg4eau4kqNuxiHP
Adds email as a second delivery channel alongside Telegram. After the audio is posted to the chat, the same reminder is emailed via Cloudflare Email Service's structured send() through the SEND_EMAIL binding. The message links to the post and the audio file rather than attaching it, which keeps it well inside the size limit. Email is best effort. By the time it runs the audio has already gone out, so throwing would leave the KV timestamp unwritten and re-post the same audio on the next tick. A failed email is reported to NOTIFICATIONS_CHAT_ID instead and the run still succeeds. EMAIL_TO is a comma separated list, and each recipient gets its own message rather than one message addressed to all of them: the recipients are mailing lists that do not need to see each other in the To header, and a rejection then fails only its own list. Every recipient is attempted before the error is raised. Messages carry Auto-Submitted: auto-generated. A list fans each message out to every member, so without it one member's out-of-office would reply back to the whole list. The send tests stub the binding, which would pass even if the payload shape were wrong, so one test hands a message to the runtime unstubbed — the local simulator rejects a message with no sender and returns a message ID for a well-formed one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UxmzyMHFg4eau4kqNuxiHP
ofa1
force-pushed
the
claude/email-notifications-49w3we
branch
from
September 6, 2026 17:13
b88a3fb to
5541ba8
Compare
A failed send put the recipient address into the thrown error, which the handler forwards to sendErrorNotification and so into the Telegram chat. That chat need not carry the recipient list, and the addresses are config, not diagnostics. Two changes, because the address can reach that message by two routes: - The aggregated error no longer interpolates the address. Which recipient failed is still logged per recipient, where the full detail belongs. - sendErrorNotification masks the local part of any address, keeping the domain. The provider's own rejection text can quote the address it rejected, which is outside this code's control, so the scrub sits at the boundary where messages leave for the chat and covers every notification path rather than the email one alone. The three new tests were confirmed to fail with either change reverted. Reported by Copilot review on #9. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UxmzyMHFg4eau4kqNuxiHP
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds email as a second delivery channel alongside Telegram. After the audio
is posted to the chat, the same reminder is emailed to the addresses in
EMAIL_TO through the SEND_EMAIL binding.
The message links to the post and the audio file rather than attaching it,
which keeps it well inside Email Routing's size limit.
Email is best effort. By the time it runs the audio has already gone out, so
throwing would leave the KV timestamp unwritten and re-post the same audio on
the next tick. A failed email is reported to NOTIFICATIONS_CHAT_ID instead and
the run still succeeds. Each recipient is attempted independently so one
unverified address cannot suppress the rest.
The RFC 5322 message is built by hand rather than with mimetext, which pulls
in @babel/runtime-corejs3 and core-js-pure for what is a string builder. The
builders are pure functions, so subject encoding, MIME structure and header
injection are covered directly by unit tests.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01UxmzyMHFg4eau4kqNuxiHP