Skip to content

feat: email each new reminder via Cloudflare Email Routing - #9

Open
ofa1 wants to merge 3 commits into
mainfrom
claude/email-notifications-49w3we
Open

feat: email each new reminder via Cloudflare Email Routing#9
ofa1 wants to merge 3 commits into
mainfrom
claude/email-notifications-49w3we

Conversation

@ofa1

@ofa1 ofa1 commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

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

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 6, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
mhmic-telegram-cron e8adfb5 Sep 06 2026, 08:38 PM

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.ts to 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.

Comment thread src/handlers.ts
ofa1 and others added 2 commits September 6, 2026 17:12
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
ofa1 force-pushed the claude/email-notifications-49w3we branch from b88a3fb to 5541ba8 Compare September 6, 2026 17:13
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants