feat(emails): Admin Telegram notification on every email sent#718
feat(emails): Admin Telegram notification on every email sent#718sweetmantech wants to merge 5 commits into
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
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 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (2)
📒 Files selected for processing (2)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 breaksendMessageand 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
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" }); |
There was a problem hiding this comment.
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>
| }); | ||
|
|
||
| // Admin Telegram ping for quality/frequency review (best-effort, non-blocking). | ||
| await notifyEmailSent({ accountId, to, cc, subject, resendId: result.id }); |
There was a problem hiding this comment.
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>
| await notifyEmailSent({ accountId, to, cc, subject, resendId: result.id }); | |
| void notifyEmailSent({ accountId, to, cc, subject, resendId: result.id }); |
…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
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
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 existingsendMessage()to the sameTELEGRAM_CHAT_IDchannel as other alerts (the credit-spend digest, error notifications).sendErrorNotification, so a Telegram failure never affects the email response.sendEmailHandlerafter a successful send (no notification on a failed send).Tests
11 emails tests green:
notifyEmailSentformats the message + omits CC when absent + swallows Telegram errors; the handler notifies on success and not on failure. tsc/lint clean.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.notifyEmailSent()to post a Markdown summary (account id,to/cc, subject, Resend id, timestamp) toTELEGRAM_CHAT_IDviasendMessage. Best-effort and non-blocking; only on success.ensureCreditsOrShortCircuitgates before sending (402 withCREDIT_AUTO_RECHARGE_FALLBACK_SUCCESS_URLwhen insufficient), andrecordCreditDeductiondeductsEMAIL_CREDIT_COST = 1credit only on success. Stampsusage_events.model_id = "POST /api/emails". No charge or notification on failures.Written for commit affd249. Summary will update on new commits.