Skip to content

feat(emails): Admin Telegram notification on every email sent#718

Open
sweetmantech wants to merge 5 commits into
testfrom
feat/emails-telegram-notify
Open

feat(emails): Admin Telegram notification on every email sent#718
sweetmantech wants to merge 5 commits into
testfrom
feat/emails-telegram-notify

Conversation

@sweetmantech

@sweetmantech sweetmantech commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Why

So the team can review the quality and frequency of outgoing email — especially agent/scheduled sends (chat#1815 observability item). Today a send is invisible.

How

  • New notifyEmailSent() (lib/emails/notifyEmailSent.ts) — formats account id, to/cc, subject, Resend id, and a timestamp into a Markdown message and posts it via the existing sendMessage() to the same TELEGRAM_CHAT_ID channel as other alerts (the credit-spend digest, error notifications).
  • Best-effort (try/catch, never throws) — mirrors sendErrorNotification, so a Telegram failure never affects the email response.
  • Wired into sendEmailHandler after a successful send (no notification on a failed send).

Tests

11 emails tests green: notifyEmailSent formats the message + omits CC when absent + swallows Telegram errors; the handler notifies on success and not on failure. tsc/lint clean.

Stacked on api#717 (the credit-charge PR) — both touch sendEmailHandler. Merge #717 first; this branches off it.

Tracked on chat#1815.

🤖 Generated with Claude Code


Summary by cubic

Send an admin Telegram alert for every successful email sent via POST /api/emails, and charge 1 credit per send. Also tags usage events and returns a controlled 500 with CORS on unexpected errors.

  • New Features
    • Added notifyEmailSent() to post a Markdown summary (account id, to/cc, subject, Resend id, timestamp) to TELEGRAM_CHAT_ID via sendMessage. Best-effort and non-blocking; only on success.
    • Per-email charging: ensureCreditsOrShortCircuit gates before sending (402 with CREDIT_AUTO_RECHARGE_FALLBACK_SUCCESS_URL when insufficient), and recordCreditDeduction deducts EMAIL_CREDIT_COST = 1 credit only on success. Stamps usage_events.model_id = "POST /api/emails". No charge or notification on failures.

Written for commit affd249. Summary will update on new commits.

Review in cubic

sweetmantech and others added 2 commits June 25, 2026 19:34
Sends were free/unmetered. Charge EMAIL_CREDIT_COST = 1 credit ($0.01): Resend's
per-email cost is <= $0.0004 (cheapest paid tier Pro $20/50k), which rounds up to
the $0.01 minimum, so 1 credit, no markup.

Pattern (matches the deep-research handler): ensureCreditsOrShortCircuit gates
first — 402 if the account can't cover it (auto-recharges via a card on file) and
does NOT deduct; then deduct only on a successful send via recordCreditDeduction
(atomic credits_usage debit + usage_events insert, source "api"). A failed send
(502) is not charged.

Tests: gate→charge on success, 402 + no send when insufficient, no charge on send
failure. 6 handler tests green; tsc/lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
So the team can review the quality + frequency of outgoing email (esp.
agent/scheduled sends). New notifyEmailSent() formats account + to/cc + subject
+ Resend id + timestamp and posts to the same TELEGRAM_CHAT_ID channel as other
alerts via the existing sendMessage(). Best-effort (try/catch, never throws),
mirroring sendErrorNotification — a Telegram failure never blocks the send.
Wired into sendEmailHandler after a successful send (not charged/notified on
failure).

Tests: notifyEmailSent formats + swallows errors; handler notifies on success,
not on failure. 11 emails tests green; tsc/lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
api Ready Ready Preview Jun 26, 2026 1:06am

Request Review

@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@sweetmantech, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 57 minutes and 16 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 32bb4e99-9064-4fb7-ac86-d9edb888372f

📥 Commits

Reviewing files that changed from the base of the PR and between cae9f35 and affd249.

⛔ Files ignored due to path filters (2)
  • lib/emails/__tests__/notifyEmailSent.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/emails/__tests__/sendEmailHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (2)
  • lib/emails/notifyEmailSent.ts
  • lib/emails/sendEmailHandler.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/emails-telegram-notify

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cubic-dev-ai cubic-dev-ai Bot 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.

2 issues found across 4 files

Confidence score: 3/5

  • In lib/emails/notifyEmailSent.ts, unescaped dynamic fields are sent as Telegram Markdown, so characters in address/subject can break sendMessage and silently drop “email sent” notifications even when delivery succeeded — escape or sanitize Markdown-sensitive characters before merging.
  • In lib/emails/sendEmailHandler.ts, awaiting the best-effort Telegram notify path keeps the API response coupled to external Telegram latency/failures, which can slow or intermittently impact client requests — make the notification fire-and-forget (with internal error logging) before merging.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="lib/emails/notifyEmailSent.ts">

<violation number="1" location="lib/emails/notifyEmailSent.ts:44">
P2: Raw dynamic fields are interpolated into Telegram Markdown without escaping. Special characters in addresses/subject can make sendMessage fail, so successful emails may not be reported.</violation>
</file>

<file name="lib/emails/sendEmailHandler.ts">

<violation number="1" location="lib/emails/sendEmailHandler.ts:78">
P2: Best-effort Telegram notification is still blocking because it is awaited. Fire-and-forget the call so external Telegram latency does not delay this API response.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant Client as External Client
    participant API as POST /api/emails
    participant Validate as validateSendEmailBody
    participant Credits as ensureCreditsOrShortCircuit
    participant Email as processAndSendEmail
    participant Resend as Resend API
    participant Deduct as recordCreditDeduction
    participant Notify as notifyEmailSent
    participant Telegram as Telegram Bot API

    Note over Client,Telegram: Email Send Flow with Credit Charging and Admin Notification

    Client->>API: POST /api/emails (body + auth)
    API->>Validate: validate request body & auth
    Validate-->>API: validated data (includes accountId)

    API->>Credits: ensureCreditsOrShortCircuit(accountId, 1 credit)
    Credits->>Credits: Check if account has sufficient credits
    alt Insufficient credits
        Credits-->>API: 402 response
        API-->>Client: 402 Insufficient Credits
    else Sufficient credits
        Credits-->>API: null (proceed)
    end

    API->>Email: processAndSendEmail(to, cc, subject, text, html)
    Email->>Resend: POST /send (external email provider)
    Resend-->>Email: resendId

    alt Email send failed
        Email-->>API: { success: false, error }
        API-->>Client: 502 Bad Gateway
    else Email sent successfully
        Email-->>API: { success: true, id: resendId }

        API->>Deduct: recordCreditDeduction(accountId, 1 credit, source: "api")
        Deduct->>Deduct: Deduct 1 credit atomically

        API->>Notify: notifyEmailSent(accountId, to, cc, subject, resendId)
        Notify->>Notify: Format Markdown message
        Notify->>Telegram: sendMessage(formatted message, parse_mode: "Markdown")
        Note over Notify,Telegram: Best-effort, never throws on failure
        opt Telegram API fails
            Notify->>Notify: Catch error, log to console
        end

        API-->>Client: 200 { success, message, id }
    end
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

*/
export async function notifyEmailSent(n: EmailSentNotification): Promise<void> {
try {
await sendMessage(formatEmailSentMessage(n), { parse_mode: "Markdown" });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Raw dynamic fields are interpolated into Telegram Markdown without escaping. Special characters in addresses/subject can make sendMessage fail, so successful emails may not be reported.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/emails/notifyEmailSent.ts, line 44:

<comment>Raw dynamic fields are interpolated into Telegram Markdown without escaping. Special characters in addresses/subject can make sendMessage fail, so successful emails may not be reported.</comment>

<file context>
@@ -0,0 +1,48 @@
+ */
+export async function notifyEmailSent(n: EmailSentNotification): Promise<void> {
+  try {
+    await sendMessage(formatEmailSentMessage(n), { parse_mode: "Markdown" });
+  } catch (err) {
+    console.error("Error in notifyEmailSent:", err);
</file context>

Comment thread lib/emails/sendEmailHandler.ts Outdated
});

// Admin Telegram ping for quality/frequency review (best-effort, non-blocking).
await notifyEmailSent({ accountId, to, cc, subject, resendId: result.id });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Best-effort Telegram notification is still blocking because it is awaited. Fire-and-forget the call so external Telegram latency does not delay this API response.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/emails/sendEmailHandler.ts, line 78:

<comment>Best-effort Telegram notification is still blocking because it is awaited. Fire-and-forget the call so external Telegram latency does not delay this API response.</comment>

<file context>
@@ -35,12 +60,23 @@ export async function sendEmailHandler(request: NextRequest): Promise<NextRespon
+  });
+
+  // Admin Telegram ping for quality/frequency review (best-effort, non-blocking).
+  await notifyEmailSent({ accountId, to, cc, subject, resendId: result.id });
+
   return NextResponse.json(
</file context>
Suggested change
await notifyEmailSent({ accountId, to, cc, subject, resendId: result.id });
void notifyEmailSent({ accountId, to, cc, subject, resendId: result.id });

sweetmantech and others added 3 commits June 25, 2026 19:50
…iew)

Per cubic review: the credit gate (ensureCreditsOrShortCircuit →
autoRechargeOrFail) makes Stripe calls and can throw, but the handler had no
try/catch, so a Stripe/DB hiccup produced an uncaught, uncontrolled 500 with no
CORS headers. Wrap the whole handler body and return a controlled
{ status: "error" } 500 with CORS on any unexpected throw.

Added a test: gate throws → 500 with CORS, no send. 7 handler tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
So endpoint usage is queryable per the request:
  select count(*) from usage_events where model_id = 'POST /api/emails'

recordCreditDeduction already accepts modelId; pass EMAIL_USAGE_MODEL_ID.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts:
#	lib/emails/sendEmailHandler.ts
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.

1 participant